Compare commits

...

21 Commits

Author SHA1 Message Date
edfea45936 Migration 2021-10-27 15:22:35 +02:00
62a82b01b8 New Crowdin updates (#369)
* New translations strings.xml (Sinhala)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

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

* Remove the LikeButton dependency

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

* Add footer options to drawer

* Apply styles to drawer

* Added sources and tags to side drawer

* Hide bottom bar when the side drawer is opened

* Display hamburger icon to open the side drawer

* Add information about libraries to the side drawer

* Cleanup

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

* Disable deprecated options

* Upgrade to latest SDK

* Upgrade kotlin

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

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

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

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

* Migrate setting articles as read from ArticleFragment to SharedItems

* Removed unused assertion

* Allow marking articles as read from the article reader

* Added contributors, because they deserve it !

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

* Migrate setting articles as read from ArticleFragment to SharedItems

* Removed unused assertion

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

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

* Remove displayed items only if displaying unread items

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

* Store articles in SharedItems when they get fetched

* Add tag filtering

* Mark items as read

* Disable sorting function

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

* Fetch items on pull gesture

* Move marking as read logic in SharedItems.

* Delegate item status to SharedItems

* Allow changing unread status of items

* Use full article position reference and not the relative one

* Delegate marking items as unread to SharedItems

* Delegate database addition of Items to SharedItems.

* Function to only provide connectivity information

* Better database management

* Sort items by date

* Provide information about item caching to SharedItems

* Add missing imports

* Update database after fetching articles

* Add missing variable

* Remove unused import

* Use coroutines to access database

* Use coroutines to simultaneously fetch articles.

* Update database after fetching articles.

* Don't block thread when accessing the database

* Prevent crash if connectivity is lost while fetching articles

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

* Use coroutines in the background sync

* Added function to get only new items

* Introduced function to filter articles

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

* Improve item filtering

* Apply filters when they are selected on the UI

* Handle infinite scroll

* Incorrect parameters were passed

* Simplify tab selection logic

* Upgrade kotlin jvm to version 1.8

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

* Remove redundant assignations.

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

* Fetch only the article in the tab selected

* Correct inconsistent position address

* Disable swiping articles only if favorites are selected

* Delegate badge count to SharedItems

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

* Remove unused functions and variables

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

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

* Adapt function to read all articles to the new changes

* Use IO Dispatcher for Database and Network computations

* Adapt Background sync to the usage of SharedItems

* Handle refresh gesture appropriately by refreshing the whole items list

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

* New translations strings.xml (Sinhala)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Catalan)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* New translations strings.xml (Korean)

* New translations strings.xml (Dutch)

* New translations strings.xml (Portuguese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Galician)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Persian)

* New translations strings.xml (Sinhala)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)

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

* Introduced functions to set articles as read globally

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

* Check new date format in items.

* Global date formatter.

* Ensure api version has been fetched before checking.

* Move api check to MyApp.

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

* Check api in Home, allow null values.
2021-03-22 19:00:42 +01:00
61 changed files with 1602 additions and 1326 deletions

View File

@ -42,6 +42,8 @@
- Closing #322. App crashed because of svg images. - Closing #322. App crashed because of svg images.
- Closing #236. New sources can be added in Selfoss 2.19.
**1.6.x** **1.6.x**
- Handling hidden tags. - Handling hidden tags.

View File

@ -36,3 +36,12 @@ If you are a user, you can still create new issues. I'll fix them when I can.
- [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1) - [See what I'm doing](https://github.com/aminecmi/ReaderforSelfoss/projects/1)
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues) - [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
- [Help translation the app](https://crowdin.com/project/readerforselfoss) - [Help translation the app](https://crowdin.com/project/readerforselfoss)
## Contributors (Alphabetical order) ❤️
- [@aancel](https://github.com/aancel)
- [@Binnette](https://github.com/Binnette)
- [@davidoskky](https://github.com/davidoskky)
- [@hectorgabucio](https://github.com/hectorgabucio)
- [@licaon-kter](https://github.com/licaon-kter)
- [@sergey-babkin](https://github.com/sergey-babkin)

View File

@ -35,15 +35,15 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
compileSdkVersion 30 compileSdkVersion 31
buildToolsVersion '30.0.3' buildToolsVersion '31.0.0'
buildFeatures { buildFeatures {
viewBinding true viewBinding true
} }
defaultConfig { defaultConfig {
applicationId "apps.amine.bou.readerforselfoss" applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 30 targetSdkVersion 31
versionCode versionCodeFromGit() versionCode versionCodeFromGit()
versionName versionNameFromGit() versionName versionNameFromGit()
@ -68,11 +68,14 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
shrinkResources false shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro' 'proguard-rules.pro'
} }
debug { debug {
buildConfigField "String", "LOGIN_URL", appLoginUrl
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
} }
} }
flavorDimensions "build" flavorDimensions "build"
@ -82,6 +85,9 @@ android {
dimension "build" dimension "build"
} }
} }
kotlinOptions {
jvmTarget = '1.8'
}
} }
dependencies { dependencies {
@ -95,14 +101,16 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Android Support // Android Support
implementation "androidx.appcompat:appcompat:1.3.0-alpha02" implementation "androidx.appcompat:appcompat:1.4.0-beta01"
implementation 'com.google.android.material:material:1.3.0-beta01' implementation 'com.google.android.material:material:1.5.0-alpha04'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01' implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01'
implementation "androidx.legacy:legacy-support-v4:$android_version" implementation "androidx.legacy:legacy-support-v4:$android_version"
implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02' implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02'
implementation "androidx.browser:browser:1.3.0" implementation "androidx.browser:browser:1.3.0"
implementation "androidx.cardview:cardview:$android_version" implementation "androidx.cardview:cardview:$android_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha2' implementation "androidx.annotation:annotation:1.2.0"
implementation 'androidx.work:work-runtime-ktx:2.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
//multidex //multidex
@ -113,23 +121,25 @@ dependencies {
transitive = true transitive = true
} }
// Async
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
// Retrofit + http logging + okhttp // Retrofit + http logging + okhttp
implementation 'com.squareup.retrofit2:retrofit:2.3.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.burgstaller:okhttp-digest:1.12' implementation 'com.burgstaller:okhttp-digest:2.5'
// Material-ish things // Material-ish things
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.1.0' 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' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
// glide // glide
implementation 'com.github.bumptech.glide:glide:4.1.1' kapt 'com.github.bumptech.glide:compiler:4.11.0'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1' implementation 'com.github.bumptech.glide:okhttp3-integration:4.1.1'
// Drawer // Drawer
implementation 'co.zsmb:materialdrawer-kt:2.0.2' implementation 'com.mikepenz:materialdrawer:8.4.4'
// Themes // Themes
implementation 'com.52inc:scoops:1.0.0' implementation 'com.52inc:scoops:1.0.0'
@ -142,13 +152,13 @@ dependencies {
//PhotoView //PhotoView
implementation 'com.github.chrisbanes:PhotoView:2.0.0' implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'androidx.core:core-ktx:1.5.0-alpha05' implementation 'androidx.core:core-ktx:1.7.0-rc01'
implementation "androidx.lifecycle:lifecycle-livedata:2.3.0-rc01" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-rc01"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01" implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01"
implementation "androidx.room:room-runtime:2.3.0-alpha04" implementation "androidx.room:room-ktx:2.4.0-beta01"
kapt "androidx.room:room-compiler:2.3.0-alpha04" kapt "androidx.room:room-compiler:2.4.0-beta01"
implementation "android.arch.work:work-runtime-ktx:$work_version" implementation "android.arch.work:work-runtime-ktx:$work_version"
} }

View File

@ -15,7 +15,8 @@
android:theme="@style/NoBar"> android:theme="@style/NoBar">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:theme="@style/SplashTheme"> android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@ -48,7 +49,8 @@
</activity> </activity>
<activity <activity
android:name=".AddSourceActivity" android:name=".AddSourceActivity"
android:parentActivityName=".SourcesActivity"> android:parentActivityName=".SourcesActivity"
android:exported="true">
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".SourcesActivity" /> android:value=".SourcesActivity" />

View File

@ -206,42 +206,78 @@ class AddSourceActivity : AppCompatActivity() {
private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) { private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
val sourceDetailsAvailable = val sourceDetailsUnavailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
if (sourceDetailsAvailable) { when {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show() sourceDetailsUnavailable -> {
} else { Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
api.createSource( }
title, PreferenceManager.getDefaultSharedPreferences(this).getInt("apiVersionMajor", 0) > 1 -> {
url, val tagList = tags.text.toString().split(",").map { it.trim() }
mSpoutsValue!!, api.createSourceApi2(
tags.text.toString(), title,
"" url,
).enqueue(object : Callback<SuccessResponse> { mSpoutsValue!!,
override fun onResponse( tagList,
call: Call<SuccessResponse>, ""
response: Response<SuccessResponse> ).enqueue(object : Callback<SuccessResponse> {
) { override fun onResponse(
if (response.body() != null && response.body()!!.isSuccess) { call: Call<SuccessResponse>,
finish() response: Response<SuccessResponse>
} else { ) {
if (response.body() != null && response.body()!!.isSuccess) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@AddSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} })
}
else -> {
api.createSource(
title,
url,
mSpoutsValue!!,
tags.text.toString(),
""
).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) { override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@AddSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) })
}
} }
} }
} }

View File

@ -60,23 +60,18 @@ class MyApp : MultiDexApplication() {
private fun initDrawerImageLoader() { private fun initDrawerImageLoader() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set( override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
imageView: ImageView?, Glide.with(imageView.context)
uri: Uri?,
placeholder: Drawable?,
tag: String?
) {
Glide.with(imageView?.context)
.loadMaybeBasicAuth(config, uri.toString()) .loadMaybeBasicAuth(config, uri.toString())
.apply(RequestOptions.fitCenterTransform().placeholder(placeholder)) .apply(RequestOptions.fitCenterTransform().placeholder(placeholder))
.into(imageView) .into(imageView)
} }
override fun cancel(imageView: ImageView?) { override fun cancel(imageView: ImageView) {
Glide.with(imageView?.context).clear(imageView) Glide.with(imageView.context).clear(imageView)
} }
override fun placeholder(ctx: Context?, tag: String?): Drawable { override fun placeholder(ctx: Context, tag: String?): Drawable {
return baseContext.resources.getDrawable(R.mipmap.ic_launcher) return baseContext.resources.getDrawable(R.mipmap.ic_launcher)
} }
}) })

View File

@ -2,6 +2,9 @@ package apps.amine.bou.readerforselfoss
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -14,17 +17,12 @@ import androidx.appcompat.app.AppCompatActivity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.room.Room import androidx.room.Room
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
@ -32,16 +30,10 @@ import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.toggleStar import apps.amine.bou.readerforselfoss.utils.toggleStar
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import me.relex.circleindicator.CircleIndicator import me.relex.circleindicator.CircleIndicator
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
class ReaderActivity : AppCompatActivity() { class ReaderActivity : AppCompatActivity() {
@ -62,8 +54,11 @@ class ReaderActivity : AppCompatActivity() {
val ALIGN_LEFT = 2 val ALIGN_LEFT = 2
private fun showMenuItem(willAddToFavorite: Boolean) { private fun showMenuItem(willAddToFavorite: Boolean) {
toolbarMenu.findItem(R.id.save).isVisible = willAddToFavorite if (willAddToFavorite) {
toolbarMenu.findItem(R.id.unsave).isVisible = !willAddToFavorite toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)
} else {
toolbarMenu.findItem(R.id.star).icon.colorFilter = PorterDuffColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP)
}
} }
private fun canFavorite() { private fun canFavorite() {
@ -146,42 +141,16 @@ class ReaderActivity : AppCompatActivity() {
} else { } else {
canFavorite() canFavorite()
} }
readItem(allItems[binding.pager.currentItem]) readItem(allItems[position])
} }
} }
) )
} }
fun readItem(item: Item) { private fun readItem(item: Item) {
if (markOnScroll) { if (markOnScroll) {
thread { SharedItems.readItem(applicationContext, api, db, item)
db.itemsDao().delete(item.toEntity())
} }
if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) {
api.markItem(item.id).enqueue(
object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
thread {
db.itemsDao().insertAllItems(item.toEntity())
}
}
}
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(item.id, true, false, false, false))
}
}
}
} }
private fun notifyAdapter() { private fun notifyAdapter() {
@ -266,62 +235,23 @@ class ReaderActivity : AppCompatActivity() {
onBackPressed() onBackPressed()
return true return true
} }
R.id.save -> { R.id.star -> {
if (this@ReaderActivity.isNetworkAccessible(null)) { if (allItems[binding.pager.currentItem].starred) {
api.starrItem(allItems[binding.pager.currentItem].id) SharedItems.unstarItem(
.enqueue(object : Callback<SuccessResponse> { this@ReaderActivity,
override fun onResponse( api,
call: Call<SuccessResponse>, db,
response: Response<SuccessResponse> allItems[binding.pager.currentItem]
) { )
afterSave() afterUnsave()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else { } else {
thread { SharedItems.starItem(
db.actionsDao().insertAllActions(ActionEntity(allItems[binding.pager.currentItem].id, false, false, true, false)) this@ReaderActivity,
afterSave() api,
} db,
} allItems[binding.pager.currentItem]
} )
R.id.unsave -> { afterSave()
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.unstarrItem(allItems[binding.pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
afterUnsave()
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
Toast.makeText(
baseContext,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[binding.pager.currentItem].id, false, false, false, true))
afterUnsave()
}
} }
} }
R.id.align_left -> { R.id.align_left -> {

View File

@ -2,29 +2,26 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView.ScaleType import android.widget.ImageView.ScaleType
import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.CardItemBinding import apps.amine.bou.readerforselfoss.databinding.CardItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener import apps.amine.bou.readerforselfoss.utils.LinkOnTouchListener
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink import apps.amine.bou.readerforselfoss.utils.shareLink
@ -33,12 +30,6 @@ import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.like.LikeButton
import com.like.OnLikeListener
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
@ -68,13 +59,13 @@ class ItemCardAdapter(
with(holder) { with(holder) {
val itm = items[position] val itm = items[position]
binding.favButton.isSelected = itm.starred
binding.favButton.isLiked = itm.starred
binding.title.text = itm.getTitleDecoded() binding.title.text = itm.getTitleDecoded()
binding.title.setTextColor(ContextCompat.getColor( binding.title.setTextColor(ContextCompat.getColor(
c, c,
appColors.textColor appColors.textColor
)) ))
binding.title.setOnTouchListener(LinkOnTouchListener()) binding.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(appColors.colorAccent) binding.title.setLinkTextColor(appColors.colorAccent)
@ -112,8 +103,6 @@ class ItemCardAdapter(
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage) c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage)
} }
binding.favButton.isLiked = itm.starred
} }
} }
@ -130,75 +119,30 @@ class ItemCardAdapter(
private fun handleClickListeners() { private fun handleClickListeners() {
binding.favButton.setOnLikeListener(object : OnLikeListener { binding.favButton.setOnClickListener {
override fun liked(likeButton: LikeButton) {
val (id) = items[bindingAdapterPosition]
if (c.isNetworkAccessible(null)) {
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
binding.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(id, false, false, true, false))
}
}
}
override fun unLiked(likeButton: LikeButton) {
val (id) = items[bindingAdapterPosition]
if (c.isNetworkAccessible(null)) {
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
binding.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
Toast.LENGTH_SHORT
).show()
}
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(id, false, false, false, true))
}
}
}
})
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition] val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded()) if (isNetworkAvailable(c)) {
if (item.starred) {
SharedItems.unstarItem(c, api, db, item)
item.starred = false
binding.favButton.isSelected = false
} else {
SharedItems.starItem(c, api, db, item)
item.starred = true
binding.favButton.isSelected = true
}
}
} }
binding.browserBtn.setOnClickListener { binding.shareBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition]) val item = items[bindingAdapterPosition]
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
}
} }
}
private fun handleCustomTabActions() { private fun handleCustomTabActions() {
val customTabsIntent = c.buildCustomTabsIntent() val customTabsIntent = c.buildCustomTabsIntent()

View File

@ -2,22 +2,12 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import android.text.Spannable
import android.text.style.ClickableSpan
import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat 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.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ListItemBinding import apps.amine.bou.readerforselfoss.databinding.ListItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
@ -27,19 +17,11 @@ import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.like.LikeButton
import com.like.OnLikeListener
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
class ItemListAdapter( class ItemListAdapter(

View File

@ -2,25 +2,16 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity import android.app.Activity
import android.graphics.Color import android.graphics.Color
import com.google.android.material.snackbar.Snackbar
import androidx.recyclerview.widget.RecyclerView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import androidx.recyclerview.widget.RecyclerView
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity import com.google.android.material.snackbar.Snackbar
import apps.amine.bou.readerforselfoss.utils.succeeded
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import kotlin.concurrent.thread
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
abstract var items: ArrayList<Item> abstract var items: ArrayList<Item>
@ -32,8 +23,8 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
abstract val config: Config abstract val config: Config
abstract val updateItems: (ArrayList<Item>) -> Unit abstract val updateItems: (ArrayList<Item>) -> Unit
fun updateAllItems(newItems: ArrayList<Item>) { fun updateAllItems() {
items = newItems items = SharedItems.focusedItems
notifyDataSetChanged() notifyDataSetChanged()
updateItems(items) updateItems(items)
} }
@ -46,34 +37,11 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
items.add(position, i) SharedItems.unreadItem(app, api, db, i)
thread { if (SharedItems.displayedItems == "unread") {
db.itemsDao().insertAllItems(i.toEntity()) addItemAtIndex(i, position)
}
notifyItemInserted(position)
updateItems(items)
if (app.isNetworkAccessible(null)) {
api.unmarkItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
items.remove(i)
thread {
db.itemsDao().delete(i.toEntity())
}
notifyItemRemoved(position)
updateItems(items)
}
})
} else { } else {
thread { notifyItemChanged(position)
db.actionsDao().insertAllActions(ActionEntity(i.id, false, true, false, false))
}
} }
} }
@ -83,7 +51,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
s.show() s.show()
} }
private fun markSnackbar(i: Item, position: Int) { private fun markSnackbar(position: Int) {
val s = Snackbar val s = Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
@ -91,34 +59,13 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
items.add(position, i) SharedItems.readItem(app, api, db, items[position])
thread { items = SharedItems.focusedItems
db.itemsDao().delete(i.toEntity()) if (SharedItems.displayedItems == "unread") {
} notifyItemRemoved(position)
notifyItemInserted(position) updateItems(items)
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 { } else {
thread { notifyItemChanged(position)
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
} }
} }
@ -129,99 +76,30 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
fun handleItemAtIndex(position: Int) { fun handleItemAtIndex(position: Int) {
if (unreadItemStatusAtIndex(position)) { if (SharedItems.unreadItemStatusAtIndex(position)) {
readItemAtIndex(position) readItemAtIndex(position)
} else { } else {
unreadItemAtIndex(position) unreadItemAtIndex(position)
} }
} }
fun unreadItemStatusAtIndex(position: Int): Boolean {
return items[position].unread
}
private fun readItemAtIndex(position: Int) { private fun readItemAtIndex(position: Int) {
val i = items[position] val i = items[position]
items.remove(i) SharedItems.readItem(app, api, db, i)
notifyItemRemoved(position) if (SharedItems.displayedItems == "unread") {
updateItems(items) items.remove(i)
notifyItemRemoved(position)
thread { updateItems(items)
db.itemsDao().delete(i.toEntity())
}
if (app.isNetworkAccessible(null)) {
api.markItem(i.id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
unmarkSnackbar(i, position)
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
app,
app.getString(R.string.cant_mark_read),
Toast.LENGTH_SHORT
).show()
items.add(position, i)
notifyItemInserted(position)
updateItems(items)
thread {
db.itemsDao().insertAllItems(i.toEntity())
}
}
})
} else { } else {
thread { notifyItemChanged(position)
db.actionsDao().insertAllActions(ActionEntity(i.id, true, false, false, false))
}
} }
unmarkSnackbar(i, position)
} }
private fun unreadItemAtIndex(position: Int) { private fun unreadItemAtIndex(position: Int) {
val i = items[position] SharedItems.unreadItem(app, api, db, items[position])
items.remove(i) notifyItemChanged(position)
notifyItemRemoved(position) markSnackbar(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))
}
}
} }
fun addItemAtIndex(item: Item, position: Int) { fun addItemAtIndex(item: Item, position: Int) {

View File

@ -3,6 +3,7 @@ package apps.amine.bou.readerforselfoss.api.selfoss
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient import apps.amine.bou.readerforselfoss.utils.getUnsafeHttpClient
import com.burgstaller.okhttp.AuthenticationCacheInterceptor import com.burgstaller.okhttp.AuthenticationCacheInterceptor
import com.burgstaller.okhttp.CachingAuthenticatorDecorator import com.burgstaller.okhttp.CachingAuthenticatorDecorator
@ -13,6 +14,8 @@ import com.burgstaller.okhttp.digest.Credentials
import com.burgstaller.okhttp.digest.DigestAuthenticator import com.burgstaller.okhttp.digest.DigestAuthenticator
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call import retrofit2.Call
import retrofit2.Retrofit import retrofit2.Retrofit
@ -67,7 +70,7 @@ class SelfossApi(
val request: Request = chain.request() val request: Request = chain.request()
val response: Response = chain.proceed(request) val response: Response = chain.proceed(request)
if (response.code() == 408) { if (response.code == 408) {
return response return response
} }
return response return response
@ -102,7 +105,7 @@ class SelfossApi(
httpClient httpClient
.addInterceptor { chain -> .addInterceptor { chain ->
val res = chain.proceed(chain.request()) val res = chain.proceed(chain.request())
if (res.code() == timeoutCode) { if (res.code == timeoutCode) {
throw SocketTimeoutException("timeout") throw SocketTimeoutException("timeout")
} }
res res
@ -116,7 +119,7 @@ class SelfossApi(
Response.Builder() Response.Builder()
.code(timeoutCode) .code(timeoutCode)
.protocol(Protocol.HTTP_2) .protocol(Protocol.HTTP_2)
.body(ResponseBody.create(MediaType.parse("text/plain"), "")) .body("".toResponseBody("text/plain".toMediaTypeOrNull()))
.message("") .message("")
.request(request) .request(request)
.build() .build()
@ -142,54 +145,50 @@ class SelfossApi(
fun login(): Call<SuccessResponse> = fun login(): Call<SuccessResponse> =
service.loginToSelfoss(config.userLogin, config.userPassword) service.loginToSelfoss(config.userLogin, config.userPassword)
fun readItems( suspend fun readItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("read", tag, sourceId, search, itemsNumber, offset) getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun newItems( suspend fun newItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("unread", tag, sourceId, search, itemsNumber, offset) getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun starredItems( suspend fun starredItems(
tag: String?,
sourceId: Long?,
search: String?,
itemsNumber: Int, itemsNumber: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
getItems("starred", tag, sourceId, search, itemsNumber, offset) getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
fun allItems(): Call<List<Item>> = fun allItems(): Call<List<Item>> =
service.allItems(userName, password) service.allItems(userName, password)
fun allNewItems(): Call<List<Item>> = suspend fun allNewItems(): retrofit2.Response<List<Item>> =
getItems("unread", null, null, null, 200, 0) getItems("unread", null, null, null, 200, 0)
fun allReadItems(): Call<List<Item>> = suspend fun allReadItems(): retrofit2.Response<List<Item>> =
getItems("read", null, null, null, 200, 0) getItems("read", null, null, null, 200, 0)
fun allStarredItems(): Call<List<Item>> = suspend fun allStarredItems(): retrofit2.Response<List<Item>> =
getItems("read", null, null, null, 200, 0) getItems("read", null, null, null, 200, 0)
private fun getItems( private suspend fun getItems(
type: String, type: String,
tag: String?, tag: String?,
sourceId: Long?, sourceId: Long?,
search: String?, search: String?,
items: Int, items: Int,
offset: Int offset: Int
): Call<List<Item>> = ): retrofit2.Response<List<Item>> =
service.getItems(type, tag, sourceId, search, userName, password, items, offset) service.getItems(type, tag, sourceId, search, null, userName, password, items, offset)
suspend fun updateItems(
updatedSince: String
): retrofit2.Response<List<Item>> =
service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0)
fun markItem(itemId: String): Call<SuccessResponse> = fun markItem(itemId: String): Call<SuccessResponse> =
service.markAsRead(itemId, userName, password) service.markAsRead(itemId, userName, password)
@ -197,7 +196,7 @@ class SelfossApi(
fun unmarkItem(itemId: String): Call<SuccessResponse> = fun unmarkItem(itemId: String): Call<SuccessResponse> =
service.unmarkAsRead(itemId, userName, password) service.unmarkAsRead(itemId, userName, password)
fun readAll(ids: List<String>): Call<SuccessResponse> = suspend fun readAll(ids: List<String>): SuccessResponse =
service.markAllAsRead(ids, userName, password) service.markAllAsRead(ids, userName, password)
fun starrItem(itemId: String): Call<SuccessResponse> = fun starrItem(itemId: String): Call<SuccessResponse> =
@ -206,8 +205,7 @@ class SelfossApi(
fun unstarrItem(itemId: String): Call<SuccessResponse> = fun unstarrItem(itemId: String): Call<SuccessResponse> =
service.unstarr(itemId, userName, password) service.unstarr(itemId, userName, password)
val stats: Call<Stats> suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password)
get() = service.stats(userName, password)
val tags: Call<List<Tag>> val tags: Call<List<Tag>>
get() = service.tags(userName, password) get() = service.tags(userName, password)
@ -215,6 +213,9 @@ class SelfossApi(
fun update(): Call<String> = fun update(): Call<String> =
service.update(userName, password) service.update(userName, password)
val apiVersion: Call<ApiVersion>
get() = service.version()
val sources: Call<List<Source>> val sources: Call<List<Source>>
get() = service.sources(userName, password) get() = service.sources(userName, password)
@ -232,4 +233,13 @@ class SelfossApi(
filter: String filter: String
): Call<SuccessResponse> = ): Call<SuccessResponse> =
service.createSource(title, url, spout, tags, filter, userName, password) service.createSource(title, url, spout, tags, filter, userName, password)
fun createSourceApi2(
title: String,
url: String,
spout: String,
tags: List<String>,
filter: String
): Call<SuccessResponse> =
service.createSourceApi2(title, url, spout, tags, filter, userName, password)
} }

View File

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

View File

@ -30,7 +30,11 @@ data class Tag(
@SerializedName("tag") val tag: String, @SerializedName("tag") val tag: String,
@SerializedName("color") val color: String, @SerializedName("color") val color: String,
@SerializedName("unread") val unread: Int @SerializedName("unread") val unread: Int
) ) {
fun getTitleDecoded(): String {
return Html.fromHtml(tag).toString()
}
}
class SuccessResponse(@SerializedName("success") val success: Boolean) { class SuccessResponse(@SerializedName("success") val success: Boolean) {
val isSuccess: Boolean val isSuccess: Boolean
@ -48,6 +52,19 @@ data class Spout(
@SerializedName("description") val description: String @SerializedName("description") val description: String
) )
data class ApiVersion(
@SerializedName("version") val version: String?,
@SerializedName("apiversion") val apiversion: String?
) {
fun getApiMajorVersion() : Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
}
data class Source( data class Source(
@SerializedName("id") val id: String, @SerializedName("id") val id: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@ -75,7 +92,7 @@ data class Item(
@SerializedName("datetime") val datetime: String, @SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String, @SerializedName("title") val title: String,
@SerializedName("content") val content: String, @SerializedName("content") val content: String,
@SerializedName("unread") val unread: Boolean, @SerializedName("unread") var unread: Boolean,
@SerializedName("starred") var starred: Boolean, @SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?, @SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?, @SerializedName("icon") val icon: String?,

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import android.os.Bundle
import android.preference.PreferenceManager import android.preference.PreferenceManager
import android.view.* import android.view.*
import android.webkit.* import android.webkit.*
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -28,25 +29,17 @@ import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
import apps.amine.bou.readerforselfoss.api.selfoss.Item import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4 import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.themes.AppColors import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config import apps.amine.bou.readerforselfoss.utils.*
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.succeeded
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
@ -56,15 +49,15 @@ import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.concurrent.thread
class ArticleFragment : Fragment() { class ArticleFragment : Fragment() {
private lateinit var pageNumber: Number private lateinit var pageNumber: Number
private var fontSize: Int = 16 private var fontSize: Int = 16
private lateinit var allItems: ArrayList<Item> private lateinit var allItems: ArrayList<Item>
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null; private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
private lateinit var url: String private lateinit var url: String
private lateinit var contentText: String private lateinit var contentText: String
private lateinit var contentSource: String private lateinit var contentSource: String
@ -85,6 +78,7 @@ class ArticleFragment : Fragment() {
private var typeface: Typeface? = null private var typeface: Typeface? = null
private var resId: Int = 0 private var resId: Int = 0
private var font = "" private var font = ""
private var staticBar = false
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
@ -112,7 +106,7 @@ class ArticleFragment : Fragment() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
try { try {
_binding = FragmentArticleBinding.inflate(inflater, container, false) _binding = FragmentArticleBinding.inflate(inflater, container, false)
@ -126,6 +120,7 @@ class ArticleFragment : Fragment() {
prefs = PreferenceManager.getDefaultSharedPreferences(activity) prefs = PreferenceManager.getDefaultSharedPreferences(activity)
editor = prefs.edit() editor = prefs.edit()
fontSize = prefs.getString("reader_font_size", "16")!!.toInt() fontSize = prefs.getString("reader_font_size", "16")!!.toInt()
staticBar = prefs.getBoolean("reader_static_bar", false)
font = prefs.getString("reader_font", "")!! font = prefs.getString("reader_font", "")!!
if (font.isNotEmpty()) { if (font.isNotEmpty()) {
@ -170,7 +165,7 @@ class ArticleFragment : Fragment() {
object : FloatingToolbar.ItemClickListener { object : FloatingToolbar.ItemClickListener {
override fun onItemClick(item: MenuItem) { override fun onItemClick(item: MenuItem) {
when (item.itemId) { when (item.itemId) {
R.id.more_action -> getContentFromMercury(customTabsIntent, prefs) R.id.more_action -> getContentFromMercury(customTabsIntent)
R.id.share_action -> requireActivity().shareLink(url, contentTitle) R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrl( R.id.open_action -> requireActivity().openItemUrl(
allItems, allItems,
@ -181,25 +176,33 @@ class ArticleFragment : Fragment() {
false, false,
requireActivity() requireActivity()
) )
R.id.unread_action -> if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) { R.id.unread_action -> if (context != null) {
api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue( if (allItems[pageNumber.toInt()].unread) {
object : Callback<SuccessResponse> { SharedItems.readItem(
override fun onResponse( context!!,
call: Call<SuccessResponse>, api,
response: Response<SuccessResponse> db,
) { allItems[pageNumber.toInt()]
} )
allItems[pageNumber.toInt()].unread = false
override fun onFailure( Toast.makeText(
call: Call<SuccessResponse>, context,
t: Throwable R.string.marked_as_read,
) { Toast.LENGTH_LONG
} ).show()
} } else {
) SharedItems.unreadItem(
} else { context!!,
thread { api,
db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false)) db,
allItems[pageNumber.toInt()]
)
allItems[pageNumber.toInt()].unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG
).show()
} }
} }
else -> Unit else -> Unit
@ -211,13 +214,18 @@ class ArticleFragment : Fragment() {
} }
) )
if (staticBar) {
fab.hide()
floatingToolbar.show()
}
binding.source.text = contentSource binding.source.text = contentSource
if (typeface != null) { if (typeface != null) {
binding.source.typeface = typeface binding.source.typeface = typeface
} }
if (contentText.isEmptyOrNullOrNullString()) { if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs) getContentFromMercury(customTabsIntent)
} else { } else {
binding.titleView.text = contentTitle binding.titleView.text = contentTitle
if (typeface != null) { if (typeface != null) {
@ -242,9 +250,14 @@ class ArticleFragment : Fragment() {
binding.nestedScrollView.setOnScrollChangeListener( binding.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY -> NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) { if (scrollY > oldScrollY) {
floatingToolbar.hide()
fab.hide() fab.hide()
} else { } else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show() if (staticBar) {
floatingToolbar.show()
} else {
if (floatingToolbar.isShowing) floatingToolbar.hide() else fab.show()
}
} }
} }
) )
@ -254,11 +267,11 @@ class ArticleFragment : Fragment() {
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message)) .setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok .setPositiveButton(android.R.string.ok
) { dialog, which -> ) { _, _ ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = sharedPref.edit() val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false) editor.putBoolean("prefer_article_viewer", false)
editor.commit() editor.apply()
requireActivity().finish() requireActivity().finish()
} }
.create() .create()
@ -282,8 +295,7 @@ class ArticleFragment : Fragment() {
} }
private fun getContentFromMercury( private fun getContentFromMercury(
customTabsIntent: CustomTabsIntent, customTabsIntent: CustomTabsIntent
prefs: SharedPreferences
) { ) {
if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) { if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
@ -334,31 +346,19 @@ class ArticleFragment : Fragment() {
} else { } else {
binding.imageView.visibility = View.GONE binding.imageView.visibility = View.GONE
} }
} catch (e: Exception) { } catch (e: Exception) { }
if (context != null) {
}
}
try { try {
binding.nestedScrollView.scrollTo(0, 0) binding.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
} catch (e: Exception) { } catch (e: Exception) { }
if (context != null) {
}
}
} else { } else {
try { try {
openInBrowserAfterFailing(customTabsIntent) openInBrowserAfterFailing(customTabsIntent)
} catch (e: Exception) { } catch (e: Exception) { }
if (context != null) {
}
}
} }
} catch (e: Exception) { } catch (e: Exception) { }
if (context != null) {
}
}
} }
override fun onFailure( override fun onFailure(
@ -431,22 +431,27 @@ class ArticleFragment : Fragment() {
override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView?, url: String): WebResourceResponse? {
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL) val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
if (url.toLowerCase().contains(".jpg") || url.toLowerCase().contains(".jpeg")) { if (url.lowercase(Locale.ROOT).contains(".jpg") || url.lowercase(Locale.ROOT).contains(".jpeg")) {
try { try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.JPEG))
}catch ( e : ExecutionException) {} }catch ( e : ExecutionException) {}
} }
else if (url.toLowerCase().contains(".png")) { else if (url.lowercase(Locale.ROOT).contains(".png")) {
try { try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG)) return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.PNG))
}catch ( e : ExecutionException) {} }catch ( e : ExecutionException) {}
} }
else if (url.toLowerCase().contains(".webp")) { else if (url.lowercase(Locale.ROOT).contains(".webp")) {
try { try {
val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get() val image = Glide.with(view).asBitmap().apply(glideOptions).load(url).submit().get()
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, Bitmap.CompressFormat.WEBP)) val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Bitmap.CompressFormat.WEBP_LOSSLESS
} else {
Bitmap.CompressFormat.WEBP
}
return WebResourceResponse("image/jpg", "UTF-8", getBitmapInputStream(image, compressFormat))
}catch ( e : ExecutionException) {} }catch ( e : ExecutionException) {}
} }

View File

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

View File

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

View File

@ -2,28 +2,21 @@ package apps.amine.bou.readerforselfoss.settings;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.EditTextPreference; import android.preference.EditTextPreference;
import android.preference.Preference; import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity; import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.preference.SwitchPreference;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
import android.text.Editable; import android.text.Editable;
import android.text.InputFilter; import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
@ -54,13 +47,10 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
* A preference value change listener that updates the preference's summary * A preference value change listener that updates the preference's summary
* to reflect its new value. * to reflect its new value.
*/ */
private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { private static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = (preference, value) -> {
@Override String stringValue = value.toString();
public boolean onPreferenceChange(Preference preference, Object value) { preference.setSummary(stringValue);
String stringValue = value.toString(); return true;
preference.setSummary(stringValue);
return true;
}
}; };
/** /**
@ -128,7 +118,7 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
loadHeadersFromResource(R.xml.pref_headers, target); loadHeadersFromResource(R.xml.pref_headers, target);
AppColors appColors = new AppColors(this); AppColors appColors = new AppColors(this);
if (appColors != null && appColors.isDarkTheme()) { if (appColors.isDarkTheme()) {
for (Header header : target) { for (Header header : target) {
tryLoadIconDark(header); tryLoadIconDark(header);
} }
@ -178,19 +168,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number"); EditTextPreference itemsNumber = (EditTextPreference) findPreference("prefer_api_items_number");
itemsNumber.getEditText().setFilters(new InputFilter[]{ itemsNumber.getEditText().setFilters(new InputFilter[]{
new InputFilter() { (source, start, end, dest, dstart, dend) -> {
try {
@Override int input = Integer.parseInt(dest.toString() + source.toString());
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { if (input <= 200 && input > 0)
try { return null;
int input = Integer.parseInt(dest.toString() + source.toString()); } catch (NumberFormatException nfe) {
if (input <= 200 && input > 0) Toast.makeText(getActivity(), R.string.items_number_should_be_number, Toast.LENGTH_LONG).show();
return null;
} catch (NumberFormatException nfe) {
Toast.makeText(getActivity(), R.string.items_number_should_be_number, Toast.LENGTH_LONG).show();
}
return "";
} }
return "";
} }
}); });
@ -214,24 +200,18 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {} public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
@Override @Override
public void afterTextChanged(Editable editable) { public void afterTextChanged(Editable editable) throws NumberFormatException {
try { fontSize.getEditText().setTextSize(Integer.parseInt(editable.toString()));
fontSize.getEditText().setTextSize(Integer.parseInt(editable.toString()));
} catch (NumberFormatException e) {}
} }
}); });
fontSize.getEditText().setFilters(new InputFilter[]{ fontSize.getEditText().setFilters(new InputFilter[]{
new InputFilter() { (source, start, end, dest, dstart, dend) -> {
try {
@Override int input = Integer.parseInt(dest.toString() + source.toString());
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { if (input > 0)
try { return null;
int input = Integer.parseInt(dest.toString() + source.toString()); } catch (NumberFormatException ignored) {}
if (input > 0) return "";
return null;
} catch (NumberFormatException nfe) {}
return "";
}
} }
}); });
} }
@ -254,28 +234,19 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
addPreferencesFromResource(R.xml.pref_links); addPreferencesFromResource(R.xml.pref_links);
setHasOptionsMenu(true); setHasOptionsMenu(true);
findPreference("trackerLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { findPreference("trackerLink").setOnPreferenceClickListener(preference -> {
@Override openUrl(Uri.parse(Config.trackerUrl));
public boolean onPreferenceClick(Preference preference) { return true;
openUrl(Uri.parse(Config.trackerUrl));
return true;
}
}); });
findPreference("sourceLink").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { findPreference("sourceLink").setOnPreferenceClickListener(preference -> {
@Override openUrl(Uri.parse(Config.sourceUrl));
public boolean onPreferenceClick(Preference preference) { return false;
openUrl(Uri.parse(Config.sourceUrl));
return false;
}
}); });
findPreference("translation").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { findPreference("translation").setOnPreferenceClickListener(preference -> {
@Override openUrl(Uri.parse(Config.translationUrl));
public boolean onPreferenceClick(Preference preference) { return false;
openUrl(Uri.parse(Config.translationUrl));
return false;
}
}); });
} }
} }

View File

@ -2,13 +2,11 @@ package apps.amine.bou.readerforselfoss.themes
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Build
import android.preference.PreferenceManager import android.preference.PreferenceManager
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.appcompat.view.ContextThemeWrapper
import android.util.TypedValue import android.util.TypedValue
import apps.amine.bou.readerforselfoss.R import apps.amine.bou.readerforselfoss.R
import android.view.LayoutInflater
import android.view.ViewGroup
class AppColors(a: Activity) { class AppColors(a: Activity) {
@ -24,26 +22,49 @@ class AppColors(a: Activity) {
init { init {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(a) val sharedPref = PreferenceManager.getDefaultSharedPreferences(a)
colorPrimary = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
sharedPref.getInt( colorPrimary =
"color_primary", sharedPref.getInt(
a.resources.getColor(R.color.colorPrimary) "color_primary",
) a.resources.getColor(R.color.colorPrimary, a.theme)
colorPrimaryDark = )
sharedPref.getInt( colorPrimaryDark =
"color_primary_dark", sharedPref.getInt(
a.resources.getColor(R.color.colorPrimaryDark) "color_primary_dark",
) a.resources.getColor(R.color.colorPrimaryDark, a.theme)
colorAccent = )
sharedPref.getInt( colorAccent =
"color_accent", sharedPref.getInt(
a.resources.getColor(R.color.colorAccent) "color_accent",
) a.resources.getColor(R.color.colorAccent, a.theme)
colorAccentDark = )
sharedPref.getInt( colorAccentDark =
"color_accent_dark", sharedPref.getInt(
a.resources.getColor(R.color.colorAccentDark) "color_accent_dark",
) a.resources.getColor(R.color.colorAccentDark, a.theme)
)
} else {
colorPrimary =
sharedPref.getInt(
"color_primary",
a.resources.getColor(R.color.colorPrimary)
)
colorPrimaryDark =
sharedPref.getInt(
"color_primary_dark",
a.resources.getColor(R.color.colorPrimaryDark)
)
colorAccent =
sharedPref.getInt(
"color_accent",
a.resources.getColor(R.color.colorAccent)
)
colorAccentDark =
sharedPref.getInt(
"color_accent_dark",
a.resources.getColor(R.color.colorAccentDark)
)
}
isDarkTheme = isDarkTheme =
sharedPref.getBoolean( sharedPref.getBoolean(
"dark_theme", "dark_theme",
@ -59,13 +80,13 @@ class AppColors(a: Activity) {
} }
textColor = if (isDarkTheme) { textColor = if (isDarkTheme) {
R.color.md_white_1000 R.color.white
} else { } else {
R.color.md_grey_900 R.color.grey_900
} }
val wrapper = Context::class.java val wrapper = Context::class.java
val method = wrapper!!.getMethod("getThemeResId") val method = wrapper.getMethod("getThemeResId")
method.isAccessible = true method.isAccessible = true
val typedCardBackground = TypedValue() val typedCardBackground = TypedValue()

View File

@ -13,7 +13,7 @@ fun String.longHash(): Long {
val chars = this.toCharArray() val chars = this.toCharArray()
for (i in 0 until l) { for (i in 0 until l) {
h = 31 * h + chars[i].toLong() h = 31 * h + chars[i].code.toLong()
} }
return h return h
} }

View File

@ -40,6 +40,8 @@ class Config(c: Context) {
const val newItemsChannelId = "new-items-channel-id" const val newItemsChannelId = "new-items-channel-id"
var dateTimeFormatter = "yyyy-MM-dd HH:mm:ss"
fun logoutAndRedirect( fun logoutAndRedirect(
c: Context, c: Context,
callingActivity: Activity, callingActivity: Activity,

View File

@ -22,7 +22,7 @@ fun String.toTextDrawableString(c: Context): String {
fun Item.sourceAndDateText(): String { fun Item.sourceAndDateText(): String {
val formattedDate: String = try { val formattedDate: String = try {
" " + DateUtils.getRelativeTimeSpanString( " " + DateUtils.getRelativeTimeSpanString(
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(this.datetime).time, SimpleDateFormat(Config.dateTimeFormatter).parse(this.datetime).time,
Date().time, Date().time,
DateUtils.MINUTE_IN_MILLIS, DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE DateUtils.FORMAT_ABBREV_RELATIVE

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import java.lang.ref.WeakReference;
*/ */
public class ServiceConnection extends CustomTabsServiceConnection { public class ServiceConnection extends CustomTabsServiceConnection {
// A weak reference to the ServiceConnectionCallback to avoid leaking it. // A weak reference to the ServiceConnectionCallback to avoid leaking it.
private WeakReference<ServiceConnectionCallback> mConnectionCallback; private final WeakReference<ServiceConnectionCallback> mConnectionCallback;
public ServiceConnection(ServiceConnectionCallback connectionCallback) { public ServiceConnection(ServiceConnectionCallback connectionCallback) {
mConnectionCallback = new WeakReference<>(connectionCallback); mConnectionCallback = new WeakReference<>(connectionCallback);

View File

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

View File

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

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,4L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,6c0,-1.11 -0.89,-2 -2,-2zM8.5,15L7.3,15l-2.55,-3.5L4.75,15L3.5,15L3.5,9h1.25l2.5,3.5L7.25,9L8.5,9v6zM13.5,10.26L11,10.26v1.12h2.5v1.26L11,12.64v1.11h2.5L13.5,15h-4L9.5,9h4v1.26zM20.5,14c0,0.55 -0.45,1 -1,1h-4c-0.55,0 -1,-0.45 -1,-1L14.5,9h1.25v4.51h1.13L16.88,9.99h1.25v3.51h1.12L19.25,9h1.25v5z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="54.751434dp" android:viewportHeight="18.756023"
android:viewportWidth="20.554007" android:width="60dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FFFFFFFF"
android:pathData="m5.7968,14.6109c-2.7907,-2.7367 -4.4957,-4.7131 -5.018,-5.8165 -2.102,-4.4408 0.2424,-8.7943 4.7357,-8.7943 1.635,0 2.7056,0.425 3.9688,1.5755l0.7937,0.723 0.7937,-0.723c1.2631,-1.1505 2.3337,-1.5755 3.9688,-1.5755 4.4933,0 6.8377,4.3535 4.7357,8.7943 -0.5223,1.1035 -2.2274,3.0799 -5.018,5.8165 -2.3248,2.2798 -4.3409,4.1451 -4.4802,4.1451 -0.1393,0 -2.1554,-1.8653 -4.4802,-4.1451z" android:strokeWidth="0.0933392"/>
</vector>

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawerContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="apps.amine.bou.readerforselfoss.HomeActivity" tools:context="apps.amine.bou.readerforselfoss.HomeActivity"
@ -36,51 +37,43 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<FrameLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/drawer_layout" android:id="@+id/swipeRefreshLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <LinearLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/windowBackground">
<LinearLayout <TextView
android:id="@+id/emptyText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="center_horizontal"
android:background="?android:attr/windowBackground"> android:paddingTop="100dp"
android:text="@string/nothing_here"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:background="@android:color/transparent"
android:visibility="gone" />
<TextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/emptyText" android:id="@+id/recyclerView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="fill" android:background="@android:color/transparent"
android:paddingTop="100dp" android:clipToPadding="false"
android:text="@string/nothing_here" android:paddingBottom="60dp"
android:textAlignment="center" android:scrollbars="vertical"
android:textAppearance="@style/TextAppearance.AppCompat.Headline" app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:background="@android:color/transparent" tools:listitem="@layout/list_item"/>
android:visibility="gone" /> </LinearLayout>
<androidx.recyclerview.widget.RecyclerView </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:clipToPadding="false"
android:paddingBottom="60dp"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/list_item"/>
</LinearLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
</LinearLayout> </LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
@ -90,4 +83,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="60dp"/> android:layout_height="60dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>
<com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
android:id="@+id/mainDrawer"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -31,8 +31,8 @@
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|bottom|right" android:layout_gravity="end|bottom|end"
android:src="@drawable/ic_add_white_24dp" app:srcCompat="@drawable/ic_add_white_24dp"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"

View File

@ -92,20 +92,22 @@
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/sourceTitleAndDate"> app:layout_constraintTop_toBottomOf="@+id/sourceTitleAndDate">
<com.like.LikeButton <ImageButton
android:id="@+id/favButton" android:id="@+id/favButton"
android:layout_width="35dp" android:layout_width="35dp"
android:layout_height="35dp" android:layout_height="35dp"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" android:layout_marginRight="8dp"
android:adjustViewBounds="true"
android:background="@android:color/transparent"
android:elevation="5dp" android:elevation="5dp"
android:padding="4dp" android:padding="4dp"
app:icon_size="22dp" android:scaleType="centerCrop"
app:icon_type="heart" /> app:srcCompat="@drawable/ic_menu_heart_60dp"
app:tint="@color/ic_menu_heart_color" />
<ImageButton <ImageButton
android:id="@+id/shareBtn" android:id="@+id/shareBtn"
@ -121,8 +123,8 @@
android:elevation="5dp" android:elevation="5dp"
android:padding="4dp" android:padding="4dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/ic_share_black_24dp" app:srcCompat="@drawable/ic_share_black_24dp"
android:tint="?android:attr/textColorPrimary" /> app:tint="?android:attr/textColorPrimary" />
<ImageButton <ImageButton
android:id="@+id/browserBtn" android:id="@+id/browserBtn"
@ -138,8 +140,8 @@
android:elevation="5dp" android:elevation="5dp"
android:padding="4dp" android:padding="4dp"
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:src="@drawable/ic_open_in_browser_black_24dp" app:srcCompat="@drawable/ic_open_in_browser_black_24dp"
android:tint="?android:attr/textColorPrimary" /> app:tint="?android:attr/textColorPrimary" />
</RelativeLayout> </RelativeLayout>

View File

@ -94,13 +94,13 @@
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|bottom|right" android:layout_gravity="end|bottom|end"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginRight="16dp" android:layout_marginRight="16dp"
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
android:src="@drawable/ic_add_white_24dp" app:srcCompat="@drawable/ic_add_white_24dp"
app:backgroundTint="?attr/colorAccent" app:backgroundTint="?attr/colorAccent"
app:fabSize="mini" app:fabSize="mini"
app:rippleColor="?attr/colorAccentDark" /> app:rippleColor="?attr/colorAccentDark" />

View File

@ -15,14 +15,8 @@
android:title="@string/reader_text_align_justify" /> android:title="@string/reader_text_align_justify" />
<item <item
android:id="@+id/unsave" android:id="@+id/star"
android:icon="@drawable/heart_on" android:icon="@drawable/ic_menu_heart_60dp"
android:title="@string/remove_to_favs_reader"
android:visible="true"
app:showAsAction="ifRoom" />
<item
android:id="@+id/save"
android:icon="@drawable/heart_off"
android:title="@string/add_to_favs_reader" android:title="@string/add_to_favs_reader"
android:visible="true" android:visible="true"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />

View File

@ -4,7 +4,7 @@
<item <item
android:id="@+id/unread_action" android:id="@+id/unread_action"
android:icon="@drawable/ic_fiber_new_white_24dp" android:icon="@drawable/ic_baseline_white_eye_24dp"
android:title="@string/unmark" android:title="@string/unmark"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
@ -17,7 +17,6 @@
<item <item
android:id="@+id/open_action" android:id="@+id/open_action"
android:icon="@drawable/ic_open_in_browser_white_24dp" android:icon="@drawable/ic_open_in_browser_white_24dp"
android:iconTint="@color/white"
android:title="@string/reader_action_open" android:title="@string/reader_action_open"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Alinear a la izquierda</string> <string name="reader_text_align_left">Alinear a la izquierda</string>
<string name="reader_text_align_justify">Justificado</string> <string name="reader_text_align_justify">Justificado</string>
<string name="settings_reader_font">Modo lectura</string> <string name="settings_reader_font">Modo lectura</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -139,8 +139,8 @@
<string name="pref_switch_items_caching_off">Les articles ne seront pas enregistrés et l\'application ne sera pas utilisable hors ligne.</string> <string name="pref_switch_items_caching_off">Les articles ne seront pas enregistrés et l\'application ne sera pas utilisable hors ligne.</string>
<string name="pref_switch_items_caching_on">Les articles seront enregistrés et l\'application sera utilisable hors ligne.</string> <string name="pref_switch_items_caching_on">Les articles seront enregistrés et l\'application sera utilisable hors ligne.</string>
<string name="pref_switch_items_caching">Sauvegarder les articles pour une utilisation hors ligne</string> <string name="pref_switch_items_caching">Sauvegarder les articles pour une utilisation hors ligne</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string> <string name="pref_switch_update_sources">Vérifier les nouvelles sources et tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string> <string name="pref_switch_update_sources_summary">Désactivez cette option si votre serveur reçoit trop de requêtes.</string>
<string name="no_network_connectivity">Hors connexion !</string> <string name="no_network_connectivity">Hors connexion !</string>
<string name="pref_switch_periodic_refresh">Synchroniser les articles</string> <string name="pref_switch_periodic_refresh">Synchroniser les articles</string>
<string name="pref_switch_periodic_refresh_off">Les articles ne seront pas synchronisés en arrière plan</string> <string name="pref_switch_periodic_refresh_off">Les articles ne seront pas synchronisés en arrière plan</string>
@ -162,4 +162,7 @@
<string name="reader_text_align_left">Aligner à gauche</string> <string name="reader_text_align_left">Aligner à gauche</string>
<string name="reader_text_align_justify">Justifier le texte</string> <string name="reader_text_align_justify">Justifier le texte</string>
<string name="settings_reader_font">Police du lecteur d\'articles</string> <string name="settings_reader_font">Police du lecteur d\'articles</string>
<string name="reader_static_bar_title">Barre statique pour le visionneur d\'articles</string>
<string name="reader_static_bar_on">La barre sera affichée</string>
<string name="reader_static_bar_off">La barre sera affichée grâce au bouton</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Aliñar á esquerda</string> <string name="reader_text_align_left">Aliñar á esquerda</string>
<string name="reader_text_align_justify">Xustificado</string> <string name="reader_text_align_justify">Xustificado</string>
<string name="settings_reader_font">Modo lector</string> <string name="settings_reader_font">Modo lector</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">左对齐</string> <string name="reader_text_align_left">左对齐</string>
<string name="reader_text_align_justify">左右对齐</string> <string name="reader_text_align_justify">左右对齐</string>
<string name="settings_reader_font">阅读器字体</string> <string name="settings_reader_font">阅读器字体</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -162,4 +162,7 @@
<string name="reader_text_align_left">Align left</string> <string name="reader_text_align_left">Align left</string>
<string name="reader_text_align_justify">Justify</string> <string name="reader_text_align_justify">Justify</string>
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -7,6 +7,7 @@
<color name="pink">#FFe91e63</color> <color name="pink">#FFe91e63</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="red">#FF0000</color>
<color name="refresh_progress_1">@color/colorAccentDark</color> <color name="refresh_progress_1">@color/colorAccentDark</color>
<color name="refresh_progress_2">@color/colorAccent</color> <color name="refresh_progress_2">@color/colorAccent</color>
<color name="refresh_progress_3">@color/pink</color> <color name="refresh_progress_3">@color/pink</color>

View File

@ -165,4 +165,7 @@
<string name="settings_reader_font">Reader font</string> <string name="settings_reader_font">Reader font</string>
<string name="open_sans_font_id" translatable="false">open_sans</string> <string name="open_sans_font_id" translatable="false">open_sans</string>
<string name="roboto_font_id" translatable="false">roboto</string> <string name="roboto_font_id" translatable="false">roboto</string>
<string name="reader_static_bar_title">Static bottom bar in the article viewer</string>
<string name="reader_static_bar_on">The bottom bar will always be displayed</string>
<string name="reader_static_bar_off">The bottom bar can be shown through the floating button</string>
</resources> </resources>

View File

@ -4,30 +4,34 @@
<item name="android:windowBackground">@drawable/background_splash</item> <item name="android:windowBackground">@drawable/background_splash</item>
</style> </style>
<style name="NoBar" parent="MaterialDrawerTheme.Light"> <style name="NoBar" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="colorAccentDark">@color/colorAccentDark</item> <item name="colorAccentDark">@color/colorAccentDark</item>
<item name="cardBackgroundColor">@color/white</item> <item name="cardBackgroundColor">@color/white</item>
<item name="android:colorBackground">@color/md_grey_50</item> <item name="android:colorBackground">@color/grey_50</item>
<item name="android:textColorPrimary">@color/md_grey_900</item> <item name="colorSurface">@color/grey_50</item>
<item name="android:textColorSecondary">@color/md_grey_400</item> <item name="android:textColorPrimary">@color/grey_900</item>
<item name="material_drawer_header_selection_text">@color/md_grey_900</item> <item name="android:textColorSecondary">@color/grey_400</item>
<item name="materialDrawerStyle">@style/Widget.MaterialDrawerStyle</item>
<item name="materialDrawerHeaderStyle">@style/Widget.MaterialDrawerHeaderStyle</item>
<item name="toolbarPopupTheme">@style/ThemeOverlay.AppCompat.Light</item> <item name="toolbarPopupTheme">@style/ThemeOverlay.AppCompat.Light</item>
</style> </style>
<style name="NoBarDark" parent="MaterialDrawerTheme"> <style name="NoBarDark" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item> <item name="colorAccent">@color/colorAccent</item>
<item name="colorAccentDark">@color/colorAccentDark</item> <item name="colorAccentDark">@color/colorAccentDark</item>
<item name="cardBackgroundColor">@color/md_grey_800</item> <item name="cardBackgroundColor">@color/grey_800</item>
<item name="android:colorBackground">@color/darkBackground</item> <item name="android:colorBackground">@color/darkBackground</item>
<item name="bnbBackgroundColor">@color/md_grey_900</item> <item name="colorSurface">@color/darkBackground</item>
<item name="android:textColorPrimary">@color/md_white_1000</item> <item name="bnbBackgroundColor">@color/grey_900</item>
<item name="android:textColorSecondary">@color/md_grey_600</item> <item name="android:textColorPrimary">@color/white</item>
<item name="material_drawer_header_selection_text">@color/md_grey_900</item> <item name="android:textColorSecondary">@color/grey_600</item>
<item name="materialDrawerStyle">@style/Widget.MaterialDrawerStyle</item>
<item name="materialDrawerHeaderStyle">@style/Widget.MaterialDrawerHeaderStyle</item>
<item name="toolbarPopupTheme">@style/ThemeOverlay.AppCompat.Dark</item> <item name="toolbarPopupTheme">@style/ThemeOverlay.AppCompat.Dark</item>
</style> </style>
@ -35,7 +39,6 @@
<style name="ToolBarStyle" parent="Theme.AppCompat"> <style name="ToolBarStyle" parent="Theme.AppCompat">
<item name="android:textColorPrimary">@color/white</item> <item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">@color/white</item> <item name="android:textColorSecondary">@color/white</item>
<item name="material_drawer_header_selection_text">@color/md_grey_900</item>
<item name="actionMenuTextColor">@color/white</item> <item name="actionMenuTextColor">@color/white</item>
<!--<item name="actionOverflowButtonStyle">@style/ActionButtonOverflowStyle</item> <!--<item name="actionOverflowButtonStyle">@style/ActionButtonOverflowStyle</item>
<item name="drawerArrowStyle">@style/DrawerArrowStyle</item>--> <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>-->

View File

@ -41,6 +41,13 @@
android:summaryOff="@string/prefer_article_viewer_off" android:summaryOff="@string/prefer_article_viewer_off"
android:summaryOn="@string/prefer_article_viewer_on" android:summaryOn="@string/prefer_article_viewer_on"
android:title="@string/prefer_article_viewer_title" /> android:title="@string/prefer_article_viewer_title" />
<SwitchPreference
android:defaultValue="false"
android:dependency="prefer_article_viewer"
android:key="reader_static_bar"
android:summaryOff="@string/reader_static_bar_off"
android:summaryOn="@string/reader_static_bar_on"
android:title="@string/reader_static_bar_title" />
<PreferenceCategory <PreferenceCategory
android:title="@string/pref_general_category_displaying"> android:title="@string/pref_general_category_displaying">

View File

@ -2,7 +2,7 @@
buildscript { buildscript {
ext { ext {
kotlin_version = '1.4.21' kotlin_version = '1.5.31'
android_version = '1.0.0' android_version = '1.0.0'
androidx_version = '1.1.0-alpha05' androidx_version = '1.1.0-alpha05'
lifecycle_version = '2.2.0-alpha01' lifecycle_version = '2.2.0-alpha01'
@ -17,7 +17,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.1' classpath 'com.android.tools.build:gradle:7.0.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }
@ -37,14 +37,4 @@ task clean(type: Delete) {
delete rootProject.buildDir delete rootProject.buildDir
} }
project.ext.preDexLibs = !project.hasProperty('disablePreDex') project.ext.preDexLibs = !project.hasProperty('disablePreDex')
subprojects {
project.plugins.whenPluginAdded { plugin ->
if ("com.android.build.gradle.AppPlugin" == plugin.class.name) {
project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
} else if ("com.android.build.gradle.LibraryPlugin" == plugin.class.name) {
project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs
}
}
}

View File

@ -16,6 +16,5 @@ org.gradle.jvmargs=-Xmx1536m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
org.gradle.caching=true org.gradle.caching=true
android.enableD8=true
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip