Compare commits

..

2 Commits

Author SHA1 Message Date
9e0fdf30ec Prevent Glide from opening svg images 2021-01-09 23:43:31 +01:00
0d84a2d7e9 Update to support rebase 2021-01-09 23:41:57 +01:00
61 changed files with 1066 additions and 1430 deletions

View File

@ -36,14 +36,6 @@
- Closing #178. Expending images on tap.
- Closing #323. Old issue with textview not having the right color.
- Closing #324. Svg images loading crashes the app.
- Closing #322. App crashed because of svg images.
- Closing #236. New sources can be added in Selfoss 2.19.
**1.6.x**
- Handling hidden tags.
@ -64,8 +56,6 @@
- Fixes #215, #208.
- Fixes #328.
**1.5.7.x**
- Added confirmation to the mark as read and update menues.

View File

@ -36,12 +36,3 @@ 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)
- [Create an issue, or request a new feature](https://github.com/aminecmi/ReaderforSelfoss/issues)
- [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

@ -30,20 +30,19 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion 31
compileSdkVersion 30
buildToolsVersion '30.0.3'
buildFeatures {
viewBinding true
}
defaultConfig {
applicationId "apps.amine.bou.readerforselfoss"
minSdkVersion 16
targetSdkVersion 31
targetSdkVersion 30
versionCode versionCodeFromGit()
versionName versionNameFromGit()
@ -68,14 +67,11 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
debug {
buildConfigField "String", "LOGIN_URL", appLoginUrl
buildConfigField "String", "LOGIN_PASSWORD", appLoginPassword
buildConfigField "String", "LOGIN_USERNAME", appLoginUsername
}
}
flavorDimensions "build"
@ -85,9 +81,6 @@ android {
dimension "build"
}
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
@ -101,14 +94,14 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Android Support
implementation "androidx.appcompat:appcompat:1.4.0-beta01"
implementation 'com.google.android.material:material:1.5.0-alpha04'
implementation 'androidx.recyclerview:recyclerview:1.3.0-alpha01'
implementation "androidx.appcompat:appcompat:1.3.0-alpha02"
implementation 'com.google.android.material:material:1.3.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
implementation "androidx.legacy:legacy-support-v4:$android_version"
implementation 'androidx.vectordrawable:vectordrawable:1.2.0-alpha02'
implementation "androidx.browser:browser:1.3.0"
implementation "androidx.cardview:cardview:$android_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha2'
implementation 'org.jsoup:jsoup:1.13.1'
//multidex
@ -119,14 +112,11 @@ dependencies {
transitive = true
}
// Async
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0'
// Retrofit + http logging + okhttp
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.1'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.burgstaller:okhttp-digest:2.5'
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
implementation 'com.burgstaller:okhttp-digest:1.12'
// Material-ish things
implementation 'com.ashokvarma.android:bottom-navigation-bar:2.1.0'
@ -151,13 +141,13 @@ dependencies {
//PhotoView
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'androidx.core:core-ktx:1.7.0-beta02'
implementation 'androidx.core:core-ktx:1.5.0-alpha05'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0-rc01"
implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0-rc01"
implementation "androidx.lifecycle:lifecycle-livedata:2.3.0-rc01"
implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0-rc01"
implementation "androidx.room:room-ktx:2.4.0-alpha05"
kapt "androidx.room:room-compiler:2.4.0-alpha05"
implementation "androidx.room:room-runtime:2.3.0-alpha04"
kapt "androidx.room:room-compiler:2.3.0-alpha04"
implementation "android.arch.work:work-runtime-ktx:$work_version"
}

View File

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

View File

@ -23,11 +23,11 @@ import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_add_source.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import android.graphics.PorterDuff
import apps.amine.bou.readerforselfoss.databinding.ActivityAddSourceBinding
@ -37,53 +37,50 @@ class AddSourceActivity : AppCompatActivity() {
private lateinit var api: SelfossApi
private lateinit var appColors: AppColors
private lateinit var binding: ActivityAddSourceBinding
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@AddSourceActivity)
super.onCreate(savedInstanceState)
binding = ActivityAddSourceBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setContentView(R.layout.activity_add_source)
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
scoop.bind(this, Toppings.PRIMARY.value, toolbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
val drawable = binding.nameInput.background
val drawable = nameInput.background
drawable.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
// TODO: clean
if(Build.VERSION.SDK_INT > 16) {
binding.nameInput.background = drawable
nameInput.background = drawable
} else{
binding.nameInput.setBackgroundDrawable(drawable)
nameInput.setBackgroundDrawable(drawable)
}
val drawable1 = binding.sourceUri.background
val drawable1 = sourceUri.background
drawable1.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
if(Build.VERSION.SDK_INT > 16) {
binding.sourceUri.background = drawable1
sourceUri.background = drawable1
} else{
binding.sourceUri.setBackgroundDrawable(drawable1)
sourceUri.setBackgroundDrawable(drawable1)
}
val drawable2 = binding.tags.background
val drawable2 = tags.background
drawable2.setColorFilter(appColors.colorAccent, PorterDuff.Mode.SRC_ATOP)
if(Build.VERSION.SDK_INT > 16) {
binding.tags.background = drawable2
tags.background = drawable2
} else{
binding.tags.setBackgroundDrawable(drawable2)
tags.setBackgroundDrawable(drawable2)
}
setSupportActionBar(binding.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
@ -101,12 +98,12 @@ class AddSourceActivity : AppCompatActivity() {
mustLoginToAddSource()
}
maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
maybeGetDetailsFromIntentSharing(intent, sourceUri, nameInput)
binding.saveBtn.setTextColor(appColors.colorAccent)
saveBtn.setTextColor(appColors.colorAccent)
binding.saveBtn.setOnClickListener {
handleSaveSource(binding.tags, binding.nameInput.text.toString(), binding.sourceUri.text.toString(), api)
saveBtn.setOnClickListener {
handleSaveSource(tags, nameInput.text.toString(), sourceUri.text.toString(), api!!)
}
}
@ -117,7 +114,7 @@ class AddSourceActivity : AppCompatActivity() {
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
mustLoginToAddSource()
} else {
handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer)
handleSpoutsSpinner(spoutsSpinner, api, progress, formContainer)
}
}
@ -206,78 +203,42 @@ class AddSourceActivity : AppCompatActivity() {
private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) {
val sourceDetailsUnavailable =
val sourceDetailsAvailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
when {
sourceDetailsUnavailable -> {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
}
PreferenceManager.getDefaultSharedPreferences(this).getInt("apiVersionMajor", 0) > 1 -> {
val tagList = tags.text.toString().split(",").map { it.trim() }
api.createSourceApi2(
title,
url,
mSpoutsValue!!,
tagList,
""
).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
if (sourceDetailsAvailable) {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
} else {
api.createSource(
title,
url,
mSpoutsValue!!,
tags.text.toString(),
""
).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
})
}
else -> {
api.createSource(
title,
url,
mSpoutsValue!!,
tags.text.toString(),
""
).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
})
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
})
}
}
}

View File

@ -18,7 +18,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuItemCompat
import androidx.core.view.doOnNextLayout
import androidx.recyclerview.widget.*
import androidx.room.Room
import androidx.work.Constraints
@ -30,7 +29,6 @@ import apps.amine.bou.readerforselfoss.adapters.ItemListAdapter
import apps.amine.bou.readerforselfoss.adapters.ItemsAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.*
import apps.amine.bou.readerforselfoss.background.LoadingWorker
import apps.amine.bou.readerforselfoss.databinding.ActivityHomeBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
@ -40,10 +38,10 @@ import apps.amine.bou.readerforselfoss.settings.SettingsActivity
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.bottombar.maybeShow
import apps.amine.bou.readerforselfoss.utils.bottombar.removeBadge
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.flattenTags
import apps.amine.bou.readerforselfoss.utils.longHash
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
@ -58,6 +56,7 @@ import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
import com.bumptech.glide.Glide
import com.ftinc.scoop.Scoop
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import com.mikepenz.materialdrawer.Drawer
import com.mikepenz.materialdrawer.holder.BadgeStyle
@ -65,9 +64,7 @@ import com.mikepenz.materialdrawer.holder.StringHolder
import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.android.synthetic.main.activity_home.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -104,10 +101,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var infiniteScroll: Boolean = false
private var lastFetchDone: Boolean = false
private var itemsCaching: Boolean = false
private var updateSources: Boolean = true
private var markOnScroll: Boolean = false
private var hiddenTags: List<String> = emptyList()
private var apiVersionMajor: Int = 0
private var periodicRefresh = false
private var refreshMinutes: Long = 360L
@ -126,10 +120,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var firstVisible: Int = 0
private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
private lateinit var settings: SharedPreferences
private lateinit var binding: ActivityHomeBinding
private var recyclerAdapter: RecyclerView.Adapter<*>? = null
private var badgeNew: Int = -1
private var badgeAll: Int = -1
private var badgeFavs: Int = -1
private var fromTabShortcut: Boolean = false
private var offlineShortcut: Boolean = false
@ -151,8 +148,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
config = Config(this@HomeActivity)
super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater)
val view = binding.root
fromTabShortcut = intent.getIntExtra("shortcutTab", -1) != -1
offlineShortcut = intent.getBooleanExtra("startOffline", false)
@ -161,11 +156,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
elementsShown = intent.getIntExtra("shortcutTab", UNREAD_SHOWN)
}
setContentView(view)
setContentView(R.layout.activity_home)
handleThemeBinding()
setSupportActionBar(binding.toolBar)
setSupportActionBar(toolBar)
db = Room.databaseBuilder(
applicationContext,
@ -191,30 +186,20 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
handleDrawer()
handleSwipeRefreshLayout()
handleSharedPrefs()
getApiMajorVersion()
getElementsAccordingToTab()
}
private fun handleSwipeRefreshLayout() {
binding.swipeRefreshLayout.setColorSchemeResources(
swipeRefreshLayout.setColorSchemeResources(
R.color.refresh_progress_1,
R.color.refresh_progress_2,
R.color.refresh_progress_3
)
binding.swipeRefreshLayout.setOnRefreshListener {
swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false
allItems = ArrayList()
lastFetchDone = false
handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch {
refreshFocusedItems(applicationContext, api, db)
getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
}
getElementsAccordingToTab()
}
val simpleItemTouchCallback =
@ -226,7 +211,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int =
if (elementsShown == FAV_SHOWN) {
if (elementsShown != UNREAD_SHOWN && elementsShown != READ_SHOWN) {
0
} else {
super.getSwipeDirs(
@ -246,10 +231,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
val i = items.elementAtOrNull(position)
if (i != null) {
val adapter = binding.recyclerView.adapter as ItemsAdapter<*>
val adapter = recyclerView.adapter as ItemsAdapter<*>
val wasItemUnread = adapter.unreadItemStatusAtIndex(position)
adapter.handleItemAtIndex(position)
if (wasItemUnread) {
badgeNew--
} else {
badgeNew++
}
reloadBadgeContent()
val tagHashes = i.tags.tags.split(",").map { it.longHash() }
@ -276,7 +269,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(binding.recyclerView)
ItemTouchHelper(simpleItemTouchCallback).attachToRecyclerView(recyclerView)
}
private fun handleBottomBar() {
@ -313,42 +306,21 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
).setActiveColorResource(R.color.pink)
.setBadgeItem(tabStarredBadge)
binding.bottomBar
bottomBar
.addItem(tabNew)
.addItem(tabArchive)
.addItem(tabStarred)
.setFirstSelectedPosition(0)
.initialise()
binding.bottomBar.setMode(BottomNavigationBar.MODE_SHIFTING)
binding.bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC)
bottomBar.setMode(BottomNavigationBar.MODE_SHIFTING)
bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC)
if (fromTabShortcut) {
binding.bottomBar.selectTab(elementsShown - 1)
bottomBar.selectTab(elementsShown - 1)
}
}
private fun getApiMajorVersion() {
api.apiVersion.enqueue(object : Callback<ApiVersion> {
override fun onFailure(call: Call<ApiVersion>, t: Throwable) {
if (apiVersionMajor >= 4) {
Config.dateTimeFormatter = "yyyy-MM-dd'T'HH:mm:ssXXX"
}
}
override fun onResponse(call: Call<ApiVersion>, response: Response<ApiVersion>) {
if(response.body() != null) {
val version = response.body() as ApiVersion
apiVersionMajor = version.getApiMajorVersion()
sharedPref.edit().putInt("apiVersionMajor", apiVersionMajor).commit()
if (apiVersionMajor >= 4) {
Config.dateTimeFormatter = "yyyy-MM-dd'T'HH:mm:ssXXX"
}
}
}
})
}
override fun onResume() {
super.onResume()
@ -359,6 +331,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
editor = settings.edit()
handleSharedPrefs()
handleDrawerItems()
handleThemeUpdate()
@ -366,29 +340,41 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
reloadLayoutManager()
if (!infiniteScroll) {
binding.recyclerView.setHasFixedSize(true)
recyclerView.setHasFixedSize(true)
} else {
handleInfiniteScroll()
}
handleBottomBarActions()
getElementsAccordingToTab()
handleRecurringTask()
handleOfflineActions()
getElementsAccordingToTab()
}
private fun getAndStoreAllItems() {
CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true
getAndStoreAllItems(applicationContext ,api, db)
this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
SharedItems.updateDatabase(db)
}
api.allItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>).filter {
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
}
}
}
})
}
override fun onStop() {
@ -408,9 +394,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false)
infiniteScroll = sharedPref.getBoolean("infinite_loading", false)
itemsCaching = sharedPref.getBoolean("items_caching", false)
SharedItems.itemsCaching = itemsCaching
updateSources = sharedPref.getBoolean("update_sources", true)
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false)
hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) {
sharedPref.getString("hidden_tags", "")!!.replace("\\s".toRegex(), "").split(",")
} else {
@ -423,13 +406,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (refreshMinutes <= 15) {
refreshMinutes = 15
}
apiVersionMajor = sharedPref.getInt("apiVersionMajor", 0)
}
private fun handleThemeBinding() {
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
@ -452,18 +433,18 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
drawer = drawer {
rootViewRes = R.id.drawer_layout
toolbar = binding.toolBar
toolbar = toolBar
actionBarDrawerToggleEnabled = true
actionBarDrawerToggleAnimated = true
showOnFirstLaunch = true
onSlide { _, p1 ->
binding.bottomBar.alpha = (1 - p1)
bottomBar.alpha = (1 - p1)
}
onClosed {
binding.bottomBar.show()
bottomBar.show()
}
onOpened {
binding.bottomBar.hide()
bottomBar.hide()
}
if (displayAccountHeader) {
@ -545,9 +526,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList()
maybeTagFilter = it
SharedItems.tagFilter = it.tag
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
if (it.unread > 0) {
@ -598,9 +577,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList()
maybeTagFilter = it
SharedItems.tagFilter = it.tag
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
if (it.unread > 0) {
@ -632,10 +609,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList()
maybeSourceFilter = tag
SharedItems.sourceIDFilter = tag.id.toLong()
SharedItems.sourceFilter = tag.title
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
if (tag.getIcon(this@HomeActivity).isNotBlank()) {
@ -665,12 +639,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
.withOnDrawerItemClickListener { _, _, _ ->
allItems = ArrayList()
maybeSourceFilter = null
SharedItems.sourceFilter = null
SharedItems.sourceIDFilter = null
maybeTagFilter = null
SharedItems.tagFilter = null
getElementsAccordingToTab()
fetchOnEmptyList()
false
}
)
@ -726,7 +696,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (maybeDrawerData.tags != null) {
thread {
val tagEntities = maybeDrawerData.tags.map { it.toEntity() }
db.drawerDataDao().deleteAllTags()
db.drawerDataDao().insertAllTags(*tagEntities.toTypedArray())
}
}
@ -734,7 +703,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
thread {
val sourceEntities =
maybeDrawerData.sources.map { it.toEntity() }
db.drawerDataDao().deleteAllSources()
db.drawerDataDao().insertAllSources(*sourceEntities.toTypedArray())
}
}
@ -762,7 +730,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
var sources: List<Source>?
fun sourcesApiCall() {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.sources.enqueue(object : Callback<List<Source>> {
override fun onResponse(
call: Call<List<Source>>?,
@ -785,7 +753,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.tags.enqueue(object : Callback<List<Tag>> {
override fun onResponse(
call: Call<List<Tag>>,
@ -819,7 +787,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
private fun reloadLayoutManager() {
val currentManager = binding.recyclerView.layoutManager
val currentManager = recyclerView.layoutManager
val layoutManager: RecyclerView.LayoutManager
// This will only update the layout manager if settings changed
@ -830,7 +798,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
calculateNoOfColumns()
)
binding.recyclerView.layoutManager = layoutManager
recyclerView.layoutManager = layoutManager
}
is GridLayoutManager ->
if (shouldBeCardView) {
@ -840,7 +808,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
recyclerView.layoutManager = layoutManager
}
else ->
if (currentManager == null) {
@ -849,7 +817,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this,
calculateNoOfColumns()
)
binding.recyclerView.layoutManager = layoutManager
recyclerView.layoutManager = layoutManager
} else {
layoutManager = StaggeredGridLayoutManager(
calculateNoOfColumns(),
@ -857,18 +825,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
)
layoutManager.gapStrategy =
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
binding.recyclerView.layoutManager = layoutManager
recyclerView.layoutManager = layoutManager
}
} else {
}
}
}
private fun handleBottomBarActions() {
binding.bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
bottomBar.setTabSelectedListener(object : BottomNavigationBar.OnTabSelectedListener {
override fun onTabUnselected(position: Int) = Unit
override fun onTabReselected(position: Int) {
val layoutManager = binding.recyclerView.adapter
val layoutManager = recyclerView.adapter
when (layoutManager) {
is StaggeredGridLayoutManager ->
@ -891,28 +860,67 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
offset = 0
lastFetchDone = false
elementsShown = position + 1
getElementsAccordingToTab()
binding.recyclerView.scrollToPosition(0)
if (itemsCaching) {
fetchOnEmptyList()
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (position) {
0 -> ArrayList(dbItems.filter { it.unread })
1 -> ArrayList(dbItems.filter { !it.unread })
2 -> ArrayList(dbItems.filter { it.starred })
else -> ArrayList(dbItems.filter { it.unread })
}
handleListResult()
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
} else {
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
getAndStoreAllItems()
}
}
}
}
} else {
when (position) {
0 -> getUnRead()
1 -> getRead()
2 -> getStarred()
else -> Unit
}
}
}
})
}
private fun fetchOnEmptyList() {
binding.recyclerView.doOnNextLayout {
if (SharedItems.focusedItems.size - 1 == getLastVisibleItem()) {
getElementsAccordingToTab(true)
}
}
}
private fun handleInfiniteScroll() {
recyclerViewScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrolled(localRecycler: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
val lastVisibleItem = getLastVisibleItem()
val manager = recyclerView.layoutManager
val lastVisibleItem: Int = when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
if (lastVisibleItem == (items.size - 1) && items.size < maxItemNumber()) {
getElementsAccordingToTab(appendResults = true)
@ -921,26 +929,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
binding.recyclerView.clearOnScrollListeners()
binding.recyclerView.addOnScrollListener(recyclerViewScrollListener)
}
private fun getLastVisibleItem() : Int {
val manager = binding.recyclerView.layoutManager
return when (manager) {
is StaggeredGridLayoutManager -> manager.findLastCompletelyVisibleItemPositions(
null
).last()
is GridLayoutManager -> manager.findLastCompletelyVisibleItemPosition()
else -> 0
}
recyclerView.clearOnScrollListeners()
recyclerView.addOnScrollListener(recyclerViewScrollListener)
}
private fun mayBeEmpty() =
if (items.isEmpty()) {
binding.emptyText.visibility = View.VISIBLE
emptyText.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
} else {
binding.emptyText.visibility = View.GONE
emptyText.visibility = View.GONE
recyclerView.visibility = View.VISIBLE
}
private fun getElementsAccordingToTab(
@ -956,58 +955,157 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
}
offset = if (appendResults) {
SharedItems.focusedItems.size - 1
offset = if (appendResults && offsetOverride === null) {
(offset + itemsNumber)
} else {
0
offsetOverride ?: 0
}
firstVisible = if (appendResults) firstVisible else 0
doGetAccordingToTab()
if (itemsCaching) {
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
}
thread {
val dbItems = db.itemsDao().items().map { it.toView() }
runOnUiThread {
if (dbItems.isNotEmpty()) {
items = when (elementsShown) {
UNREAD_SHOWN -> ArrayList(dbItems.filter { it.unread })
READ_SHOWN -> ArrayList(dbItems.filter { !it.unread })
FAV_SHOWN -> ArrayList(dbItems.filter { it.starred })
else -> ArrayList(dbItems.filter { it.unread })
}
handleListResult()
doGetAccordingToTab()
} else {
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
doGetAccordingToTab()
getAndStoreAllItems()
}
}
}
}
} else {
doGetAccordingToTab()
}
}
private fun filter(tags: String): Boolean {
val tagsList = tags.replace("\\s".toRegex(), "").split(",")
return tagsList.intersect(hiddenTags).isEmpty()
}
private fun doCallTo(
appendResults: Boolean,
toastMessage: Int,
call: (String?, Long?, String?) -> Call<List<Item>>
) {
fun handleItemsResponse(response: Response<List<Item>>) {
val shouldUpdate = (response.body()?.toSet() != items.toSet())
if (response.body() != null) {
if (shouldUpdate) {
getAndStoreAllItems()
items = response.body() as ArrayList<Item>
items = items.filter {
maybeTagFilter != null || filter(it.tags.tags)
} as ArrayList<Item>
if (allItems.isEmpty()) {
allItems = items
} else {
items.forEach {
if (!allItems.contains(it)) allItems.add(it)
}
}
}
} else {
if (!appendResults) {
items = ArrayList()
allItems = ArrayList()
}
}
handleListResult(appendResults)
if (!appendResults) mayBeEmpty()
swipeRefreshLayout.isRefreshing = false
}
if (!swipeRefreshLayout.isRefreshing) {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = true }
}
if (this@HomeActivity.isNetworkAccessible(this@HomeActivity.findViewById(R.id.coordLayout), offlineShortcut)) {
call(maybeTagFilter?.tag, maybeSourceFilter?.id?.toLong(), maybeSearchFilter)
.enqueue(object : Callback<List<Item>> {
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
handleItemsResponse(response)
}
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
swipeRefreshLayout.isRefreshing = false
Toast.makeText(
this@HomeActivity,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
})
} else {
swipeRefreshLayout.post { swipeRefreshLayout.isRefreshing = false }
}
}
private fun getUnRead(appendResults: Boolean = false) {
CoroutineScope(Dispatchers.Main).launch {
if (appendResults || !SharedItems.fetchedUnread) {
binding.swipeRefreshLayout.isRefreshing = true
getUnreadItems(applicationContext, api, db, offset)
binding.swipeRefreshLayout.isRefreshing = false
}
SharedItems.getUnRead()
items = SharedItems.focusedItems
handleListResult()
elementsShown = UNREAD_SHOWN
doCallTo(appendResults, R.string.cant_get_new_elements) { t, id, f ->
api.newItems(
t,
id,
f,
itemsNumber,
offset
)
}
}
private fun getRead(appendResults: Boolean = false) {
CoroutineScope(Dispatchers.Main).launch {
if (appendResults || !SharedItems.fetchedAll) {
binding.swipeRefreshLayout.isRefreshing = true
getReadItems(applicationContext, api, db, offset)
binding.swipeRefreshLayout.isRefreshing = false
}
SharedItems.getAll()
items = SharedItems.focusedItems
handleListResult()
elementsShown = READ_SHOWN
doCallTo(appendResults, R.string.cant_get_read) { t, id, f ->
api.readItems(
t,
id,
f,
itemsNumber,
offset
)
}
}
private fun getStarred(appendResults: Boolean = false) {
CoroutineScope(Dispatchers.Main).launch {
if (appendResults || !SharedItems.fetchedStarred) {
binding.swipeRefreshLayout.isRefreshing = true
getStarredItems(applicationContext, api, db, offset)
binding.swipeRefreshLayout.isRefreshing = false
}
SharedItems.getStarred()
items = SharedItems.focusedItems
handleListResult()
elementsShown = FAV_SHOWN
doCallTo(appendResults, R.string.cant_get_favs) { t, id, f ->
api.starredItems(
t,
id,
f,
itemsNumber,
offset
)
}
}
private fun handleListResult(appendResults: Boolean = false) {
if (appendResults) {
val oldManager = binding.recyclerView.layoutManager
val oldManager = recyclerView.layoutManager
firstVisible = when (oldManager) {
is StaggeredGridLayoutManager ->
oldManager.findFirstCompletelyVisibleItemPositions(null).last()
@ -1052,44 +1150,68 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
updateItems(it)
}
binding.recyclerView.addItemDecoration(
recyclerView.addItemDecoration(
DividerItemDecoration(
this@HomeActivity,
DividerItemDecoration.VERTICAL
)
)
}
binding.recyclerView.adapter = recyclerAdapter
recyclerView.adapter = recyclerAdapter
} else {
(recyclerAdapter as ItemsAdapter<*>).updateAllItems()
if (!appendResults) {
(recyclerAdapter as ItemsAdapter<*>).updateAllItems(items)
} else {
(recyclerAdapter as ItemsAdapter<*>).addItemsAtEnd(items)
}
}
reloadBadges()
mayBeEmpty()
}
private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) {
CoroutineScope(Dispatchers.Main).launch {
reloadBadges(applicationContext, api)
reloadBadgeContent()
}
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && (displayUnreadCount || displayAllCount)) {
api.stats.enqueue(object : Callback<Stats> {
override fun onResponse(call: Call<Stats>, response: Response<Stats>) {
if (response.body() != null) {
badgeNew = response.body()!!.unread
badgeAll = response.body()!!.total
badgeFavs = response.body()!!.starred
reloadBadgeContent()
}
}
override fun onFailure(call: Call<Stats>, t: Throwable) {
}
})
} else {
reloadBadgeContent(succeeded = false)
}
}
private fun reloadBadgeContent() {
if (displayUnreadCount) {
tabNewBadge
.setText(SharedItems.badgeUnread.toString())
.maybeShow()
}
if (displayAllCount) {
tabArchiveBadge
.setText(SharedItems.badgeAll.toString())
.maybeShow()
tabStarredBadge
.setText(SharedItems.badgeStarred.toString())
.maybeShow()
private fun reloadBadgeContent(succeeded: Boolean = true) {
if (succeeded) {
if (displayUnreadCount) {
tabNewBadge
.setText(badgeNew.toString())
.maybeShow()
}
if (displayAllCount) {
tabArchiveBadge
.setText(badgeAll.toString())
.maybeShow()
tabStarredBadge
.setText(badgeFavs.toString())
.maybeShow()
} else {
tabArchiveBadge.removeBadge()
tabStarredBadge.removeBadge()
}
} else {
tabNewBadge.removeBadge()
tabArchiveBadge.removeBadge()
tabStarredBadge.removeBadge()
}
}
@ -1109,18 +1231,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onQueryTextChange(p0: String?): Boolean {
if (p0.isNullOrBlank()) {
maybeSearchFilter = null
SharedItems.searchFilter = null
getElementsAccordingToTab()
fetchOnEmptyList()
}
return false
}
override fun onQueryTextSubmit(p0: String?): Boolean {
maybeSearchFilter = p0
SharedItems.searchFilter = p0
getElementsAccordingToTab()
fetchOnEmptyList()
return false
}
@ -1190,33 +1308,64 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
R.id.readAll -> {
if (elementsShown == UNREAD_SHOWN) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true
swipeRefreshLayout.isRefreshing = false
val ids = allItems.map { it.id }
val itemsByTag: Map<Long, Int> =
allItems.flattenTags()
.groupBy { it.tags.tags.longHash() }
.map { it.key to it.value.size }
.toMap()
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch {
val success = readAll(applicationContext, api, db)
if (success) {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
if (ids.isNotEmpty() && this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
api.readAll(ids).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
handleDrawerItems()
handleDrawerItems()
getElementsAccordingToTab()
} else {
getElementsAccordingToTab()
} else {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
}
swipeRefreshLayout.isRefreshing = false
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
swipeRefreshLayout.isRefreshing = false
}
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
}
})
items = ArrayList()
allItems = ArrayList()
}
if (items.isEmpty()) {
Toast.makeText(
this@HomeActivity,
R.string.nothing_here,
Toast.LENGTH_SHORT
).show()
}
handleListResult()
}
}
return true
@ -1230,10 +1379,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun maxItemNumber(): Int =
when (elementsShown) {
UNREAD_SHOWN -> SharedItems.badgeUnread
READ_SHOWN -> SharedItems.badgeAll
FAV_SHOWN -> SharedItems.badgeStarred
else -> SharedItems.badgeUnread // if !elementsShown then unread are fetched.
UNREAD_SHOWN -> badgeNew
READ_SHOWN -> badgeAll
FAV_SHOWN -> badgeFavs
else -> badgeNew // if !elementsShown then unread are fetched.
}
private fun updateItems(adapterItems: ArrayList<Item>) {
@ -1277,7 +1426,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
}
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch {
thread {
val actions = db.actionsDao().actions()
actions.forEach { action ->

View File

@ -5,31 +5,27 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.fragments.ImageFragment
import kotlinx.android.synthetic.main.activity_reader.*
class ImageActivity : AppCompatActivity() {
private lateinit var allImages : ArrayList<String>
private var position : Int = 0
private lateinit var binding: ActivityImageBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityImageBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setContentView(R.layout.activity_image)
setSupportActionBar(binding.toolBar)
setSupportActionBar(toolBar)
supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
allImages = intent.getStringArrayListExtra("allImages") as ArrayList<String>
position = intent.getIntExtra("position", 0)
binding.pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager)
binding.pager.currentItem = position
pager.adapter = ScreenSlidePagerAdapter(supportFragmentManager)
pager.currentItem = position
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View File

@ -17,13 +17,13 @@ import android.widget.TextView
import android.widget.Toast
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ActivityLoginBinding
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.isBaseUrlValid
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.android.synthetic.main.activity_login.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -39,18 +39,15 @@ class LoginActivity : AppCompatActivity() {
private lateinit var editor: SharedPreferences.Editor
private lateinit var userIdentifier: String
private lateinit var appColors: AppColors
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@LoginActivity)
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setContentView(R.layout.activity_login)
setSupportActionBar(binding.toolbar)
setSupportActionBar(toolbar)
handleBaseUrlFail()
@ -68,14 +65,14 @@ class LoginActivity : AppCompatActivity() {
private fun handleActions() {
binding.withSelfhostedCert.setOnCheckedChangeListener { _, b ->
withSelfhostedCert.setOnCheckedChangeListener { _, b ->
isWithSelfSignedCert = !isWithSelfSignedCert
val visi: Int = if (b) View.VISIBLE else View.GONE
binding.warningText.visibility = visi
warningText.visibility = visi
}
binding.passwordView.setOnEditorActionListener(
passwordView.setOnEditorActionListener(
TextView.OnEditorActionListener { _, id, _ ->
if (id == R.id.loginView || id == EditorInfo.IME_NULL) {
attemptLogin()
@ -85,22 +82,22 @@ class LoginActivity : AppCompatActivity() {
}
)
binding.signInButton.setOnClickListener { attemptLogin() }
signInButton.setOnClickListener { attemptLogin() }
binding.withLogin.setOnCheckedChangeListener { _, b ->
withLogin.setOnCheckedChangeListener { _, b ->
isWithLogin = !isWithLogin
val visi: Int = if (b) View.VISIBLE else View.GONE
binding.loginLayout.visibility = visi
binding.passwordLayout.visibility = visi
loginLayout.visibility = visi
passwordLayout.visibility = visi
}
binding.withHttpLogin.setOnCheckedChangeListener { _, b ->
withHttpLogin.setOnCheckedChangeListener { _, b ->
isWithHTTPLogin = !isWithHTTPLogin
val visi: Int = if (b) View.VISIBLE else View.GONE
binding.httpLoginInput.visibility = visi
binding.httpPasswordInput.visibility = visi
httpLoginInput.visibility = visi
httpPasswordInput.visibility = visi
}
}
@ -127,25 +124,25 @@ class LoginActivity : AppCompatActivity() {
private fun attemptLogin() {
// Reset errors.
binding.urlView.error = null
binding.loginView.error = null
binding.httpLoginView.error = null
binding.passwordView.error = null
binding.httpPasswordView.error = null
urlView.error = null
loginView.error = null
httpLoginView.error = null
passwordView.error = null
httpPasswordView.error = null
// Store values at the time of the login attempt.
val url = binding.urlView.text.toString()
val login = binding.loginView.text.toString()
val httpLogin = binding.httpLoginView.text.toString()
val password = binding.passwordView.text.toString()
val httpPassword = binding.httpPasswordView.text.toString()
val url = urlView.text.toString()
val login = loginView.text.toString()
val httpLogin = httpLoginView.text.toString()
val password = passwordView.text.toString()
val httpPassword = httpPasswordView.text.toString()
var cancel = false
var focusView: View? = null
if (!url.isBaseUrlValid(this@LoginActivity)) {
binding.urlView.error = getString(R.string.login_url_problem)
focusView = binding.urlView
urlView.error = getString(R.string.login_url_problem)
focusView = urlView
cancel = true
inValidCount++
if (inValidCount == 3) {
@ -164,28 +161,28 @@ class LoginActivity : AppCompatActivity() {
if (isWithLogin) {
if (TextUtils.isEmpty(password)) {
binding.passwordView.error = getString(R.string.error_invalid_password)
focusView = binding.passwordView
passwordView.error = getString(R.string.error_invalid_password)
focusView = passwordView
cancel = true
}
if (TextUtils.isEmpty(login)) {
binding.loginView.error = getString(R.string.error_field_required)
focusView = binding.loginView
loginView.error = getString(R.string.error_field_required)
focusView = loginView
cancel = true
}
}
if (isWithHTTPLogin) {
if (TextUtils.isEmpty(httpPassword)) {
binding.httpPasswordView.error = getString(R.string.error_invalid_password)
focusView = binding.httpPasswordView
httpPasswordView.error = getString(R.string.error_invalid_password)
focusView = httpPasswordView
cancel = true
}
if (TextUtils.isEmpty(httpLogin)) {
binding.httpLoginView.error = getString(R.string.error_field_required)
focusView = binding.httpLoginView
httpLoginView.error = getString(R.string.error_field_required)
focusView = httpLoginView
cancel = true
}
}
@ -219,11 +216,11 @@ class LoginActivity : AppCompatActivity() {
editor.remove("password")
editor.remove("httpPassword")
editor.apply()
binding.urlView.error = getString(R.string.wrong_infos)
binding.loginView.error = getString(R.string.wrong_infos)
binding.passwordView.error = getString(R.string.wrong_infos)
binding.httpLoginView.error = getString(R.string.wrong_infos)
binding.httpPasswordView.error = getString(R.string.wrong_infos)
urlView.error = getString(R.string.wrong_infos)
loginView.error = getString(R.string.wrong_infos)
passwordView.error = getString(R.string.wrong_infos)
httpLoginView.error = getString(R.string.wrong_infos)
httpPasswordView.error = getString(R.string.wrong_infos)
showProgress(false)
}
@ -251,28 +248,28 @@ class LoginActivity : AppCompatActivity() {
private fun showProgress(show: Boolean) {
val shortAnimTime = resources.getInteger(android.R.integer.config_shortAnimTime)
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
binding.loginForm
loginForm.visibility = if (show) View.GONE else View.VISIBLE
loginForm
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 0F else 1F
).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginForm.visibility = if (show) View.GONE else View.VISIBLE
loginForm.visibility = if (show) View.GONE else View.VISIBLE
}
}
)
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
binding.loginProgress
loginProgress.visibility = if (show) View.VISIBLE else View.GONE
loginProgress
.animate()
.setDuration(shortAnimTime.toLong())
.alpha(
if (show) 1F else 0F
).setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.loginProgress.visibility = if (show) View.VISIBLE else View.GONE
loginProgress.visibility = if (show) View.VISIBLE else View.GONE
}
}
)

View File

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

View File

@ -20,8 +20,6 @@ import androidx.room.Room
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.databinding.ActivityReaderBinding
import apps.amine.bou.readerforselfoss.fragments.ArticleFragment
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
@ -32,12 +30,12 @@ import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.transformers.DepthPageTransformer
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import apps.amine.bou.readerforselfoss.utils.succeeded
import apps.amine.bou.readerforselfoss.utils.toggleStar
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_reader.*
import me.relex.circleindicator.CircleIndicator
import retrofit2.Call
import retrofit2.Callback
@ -56,7 +54,6 @@ class ReaderActivity : AppCompatActivity() {
private lateinit var db: AppDatabase
private lateinit var prefs: SharedPreferences
private lateinit var binding: ActivityReaderBinding
private var activeAlignment: Int = 1
val JUSTIFY = 1
@ -79,10 +76,8 @@ class ReaderActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityReaderBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setContentView(R.layout.activity_reader)
db = Room.databaseBuilder(
applicationContext,
@ -90,12 +85,12 @@ class ReaderActivity : AppCompatActivity() {
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolBar)
scoop.bind(this, Toppings.PRIMARY.value, toolBar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
setSupportActionBar(binding.toolBar)
setSupportActionBar(toolBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
@ -124,9 +119,9 @@ class ReaderActivity : AppCompatActivity() {
readItem(allItems[currentItem])
binding.pager.adapter =
pager.adapter =
ScreenSlidePagerAdapter(supportFragmentManager, AppColors(this@ReaderActivity))
binding.pager.currentItem = currentItem
pager.currentItem = currentItem
}
override fun onResume() {
@ -134,10 +129,10 @@ class ReaderActivity : AppCompatActivity() {
notifyAdapter()
binding.pager.setPageTransformer(true, DepthPageTransformer())
(binding.indicator as CircleIndicator).setViewPager(binding.pager)
pager.setPageTransformer(true, DepthPageTransformer())
(indicator as CircleIndicator).setViewPager(pager)
binding.pager.addOnPageChangeListener(
pager.addOnPageChangeListener(
object : ViewPager.SimpleOnPageChangeListener() {
override fun onPageSelected(position: Int) {
@ -147,32 +142,58 @@ class ReaderActivity : AppCompatActivity() {
} else {
canFavorite()
}
readItem(allItems[position])
readItem(allItems[pager.currentItem])
}
}
)
}
private fun readItem(item: Item) {
fun readItem(item: Item) {
if (markOnScroll) {
SharedItems.readItem(applicationContext, api, db, item)
thread {
db.itemsDao().delete(item.toEntity())
}
if (this@ReaderActivity.isNetworkAccessible(this@ReaderActivity.findViewById(R.id.reader_activity_view))) {
api.markItem(item.id).enqueue(
object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
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() {
(binding.pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
(pager.adapter as ScreenSlidePagerAdapter).notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
if (markOnScroll) {
binding.pager.clearOnPageChangeListeners()
pager.clearOnPageChangeListeners()
}
}
override fun onSaveInstanceState(oldInstanceState: Bundle) {
super.onSaveInstanceState(oldInstanceState)
oldInstanceState.clear()
oldInstanceState!!.clear()
}
private inner class ScreenSlidePagerAdapter(fm: FragmentManager, val appColors: AppColors) :
@ -224,14 +245,14 @@ class ReaderActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
fun afterSave() {
allItems[binding.pager.currentItem] =
allItems[binding.pager.currentItem].toggleStar()
allItems[pager.currentItem] =
allItems[pager.currentItem].toggleStar()
notifyAdapter()
canRemoveFromFavorite()
}
fun afterUnsave() {
allItems[binding.pager.currentItem] = allItems[binding.pager.currentItem].toggleStar()
allItems[pager.currentItem] = allItems[pager.currentItem].toggleStar()
notifyAdapter()
canFavorite()
}
@ -243,7 +264,7 @@ class ReaderActivity : AppCompatActivity() {
}
R.id.save -> {
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.starrItem(allItems[binding.pager.currentItem].id)
api.starrItem(allItems[pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
@ -265,14 +286,14 @@ class ReaderActivity : AppCompatActivity() {
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[binding.pager.currentItem].id, false, false, true, false))
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, true, false))
afterSave()
}
}
}
R.id.unsave -> {
if (this@ReaderActivity.isNetworkAccessible(null)) {
api.unstarrItem(allItems[binding.pager.currentItem].id)
api.unstarrItem(allItems[pager.currentItem].id)
.enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
@ -294,7 +315,7 @@ class ReaderActivity : AppCompatActivity() {
})
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[binding.pager.currentItem].id, false, false, false, true))
db.actionsDao().insertAllActions(ActionEntity(allItems[pager.currentItem].id, false, false, false, true))
afterUnsave()
}
}

View File

@ -12,13 +12,12 @@ import android.widget.Toast
import apps.amine.bou.readerforselfoss.adapters.SourcesListAdapter
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.Source
import apps.amine.bou.readerforselfoss.databinding.ActivityImageBinding
import apps.amine.bou.readerforselfoss.databinding.ActivitySourcesBinding
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.themes.Toppings
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import com.ftinc.scoop.Scoop
import kotlinx.android.synthetic.main.activity_sources.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -26,34 +25,31 @@ import retrofit2.Response
class SourcesActivity : AppCompatActivity() {
private lateinit var appColors: AppColors
private lateinit var binding: ActivitySourcesBinding
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@SourcesActivity)
super.onCreate(savedInstanceState)
binding = ActivitySourcesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setContentView(R.layout.activity_sources)
val scoop = Scoop.getInstance()
scoop.bind(this, Toppings.PRIMARY.value, binding.toolbar)
scoop.bind(this, Toppings.PRIMARY.value, toolbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scoop.bindStatusBar(this, Toppings.PRIMARY_DARK.value)
}
setSupportActionBar(binding.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
binding.fab.rippleColor = appColors.colorAccentDark
binding.fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
fab.rippleColor = appColors.colorAccentDark
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
}
override fun onStop() {
super.onStop()
binding.recyclerView.clearOnScrollListeners()
recyclerView.clearOnScrollListeners()
}
override fun onResume() {
@ -72,8 +68,8 @@ class SourcesActivity : AppCompatActivity() {
)
var items: ArrayList<Source> = ArrayList()
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAccessible(this@SourcesActivity.findViewById(R.id.recyclerView))) {
api.sources.enqueue(object : Callback<List<Source>> {
@ -85,7 +81,7 @@ class SourcesActivity : AppCompatActivity() {
items = response.body() as ArrayList<Source>
}
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
binding.recyclerView.adapter = mAdapter
recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged()
if (items.isEmpty()) {
Toast.makeText(
@ -106,7 +102,7 @@ class SourcesActivity : AppCompatActivity() {
})
}
binding.fab.setOnClickListener {
fab.setOnClickListener {
startActivity(Intent(this@SourcesActivity, AddSourceActivity::class.java))
}
}

View File

@ -2,6 +2,7 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity
import android.content.Context
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
@ -13,7 +14,6 @@ import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.databinding.CardItemBinding
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.themes.AppColors
@ -34,6 +34,11 @@ import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide
import com.like.LikeButton
import com.like.OnLikeListener
import kotlinx.android.synthetic.main.card_item.view.*
import kotlinx.android.synthetic.main.card_item.view.itemImage
import kotlinx.android.synthetic.main.card_item.view.sourceTitleAndDate
import kotlinx.android.synthetic.main.card_item.view.title
import kotlinx.android.synthetic.main.list_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -59,79 +64,77 @@ class ItemCardAdapter(
c.resources.getDimension(R.dimen.card_image_max_height).toInt()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
val v = LayoutInflater.from(c).inflate(R.layout.card_item, parent, false) as CardView
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder) {
val itm = items[position]
val itm = items[position]
binding.favButton.isLiked = itm.starred
binding.title.text = itm.getTitleDecoded()
binding.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
binding.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.favButton.isLiked = itm.starred
holder.mView.title.text = itm.getTitleDecoded()
holder.mView.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(appColors.colorAccent)
holder.mView.title.setLinkTextColor(appColors.colorAccent)
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
holder.mView.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
if (!fullHeightCards) {
binding.itemImage.maxHeight = imageMaxHeight
binding.itemImage.scaleType = ScaleType.CENTER_CROP
}
if (itm.getThumbnail(c).isEmpty()) {
binding.itemImage.visibility = View.GONE
Glide.with(c).clear(binding.itemImage)
binding.itemImage.setImageDrawable(null)
} else {
binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
}
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage)
}
binding.favButton.isLiked = itm.starred
if (!fullHeightCards) {
holder.mView.itemImage.maxHeight = imageMaxHeight
holder.mView.itemImage.scaleType = ScaleType.CENTER_CROP
}
if (itm.getThumbnail(c).isEmpty()) {
holder.mView.itemImage.visibility = View.GONE
Glide.with(c).clear(holder.mView.itemImage)
holder.mView.itemImage.setImageDrawable(null)
} else {
holder.mView.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(c), holder.mView.itemImage)
}
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
holder.mView.sourceImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.sourceImage)
}
holder.mView.favButton.isLiked = itm.starred
}
override fun getItemCount(): Int {
return items.size
}
inner class ViewHolder(val binding: CardItemBinding) : RecyclerView.ViewHolder(binding.root) {
inner class ViewHolder(val mView: CardView) : RecyclerView.ViewHolder(mView) {
init {
binding.root.setCardBackgroundColor(appColors.cardBackgroundColor)
mView.setCardBackgroundColor(appColors.cardBackgroundColor)
handleClickListeners()
handleCustomTabActions()
}
private fun handleClickListeners() {
binding.favButton.setOnLikeListener(object : OnLikeListener {
mView.favButton.setOnLikeListener(object : OnLikeListener {
override fun liked(likeButton: LikeButton) {
val (id) = items[bindingAdapterPosition]
val (id) = items[adapterPosition]
if (c.isNetworkAccessible(null)) {
api.starrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
@ -144,7 +147,7 @@ class ItemCardAdapter(
call: Call<SuccessResponse>,
t: Throwable
) {
binding.favButton.isLiked = false
mView.favButton.isLiked = false
Toast.makeText(
c,
R.string.cant_mark_favortie,
@ -160,7 +163,7 @@ class ItemCardAdapter(
}
override fun unLiked(likeButton: LikeButton) {
val (id) = items[bindingAdapterPosition]
val (id) = items[adapterPosition]
if (c.isNetworkAccessible(null)) {
api.unstarrItem(id).enqueue(object : Callback<SuccessResponse> {
override fun onResponse(
@ -173,7 +176,7 @@ class ItemCardAdapter(
call: Call<SuccessResponse>,
t: Throwable
) {
binding.favButton.isLiked = true
mView.favButton.isLiked = true
Toast.makeText(
c,
R.string.cant_unmark_favortie,
@ -189,13 +192,13 @@ class ItemCardAdapter(
}
})
binding.shareBtn.setOnClickListener {
val item = items[bindingAdapterPosition]
mView.shareBtn.setOnClickListener {
val item = items[adapterPosition]
c.shareLink(item.getLinkDecoded(), item.getTitleDecoded())
}
binding.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[bindingAdapterPosition])
mView.browserBtn.setOnClickListener {
c.openInBrowserAsNewTask(items[adapterPosition])
}
}
@ -203,11 +206,11 @@ class ItemCardAdapter(
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
binding.root.setOnClickListener {
mView.setOnClickListener {
c.openItemUrl(
items,
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
adapterPosition,
items[adapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,

View File

@ -2,13 +2,22 @@ package apps.amine.bou.readerforselfoss.adapters
import android.app.Activity
import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
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.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.databinding.ListItemBinding
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.Config
@ -17,11 +26,20 @@ import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.bitmapCenterCrop
import apps.amine.bou.readerforselfoss.utils.glide.circularBitmapDrawable
import apps.amine.bou.readerforselfoss.utils.openInBrowserAsNewTask
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.toTextDrawableString
import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import com.like.LikeButton
import com.like.OnLikeListener
import kotlinx.android.synthetic.main.list_item.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import kotlin.collections.ArrayList
class ItemListAdapter(
@ -41,57 +59,59 @@ class ItemListAdapter(
private val c: Context = app.baseContext
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
val v = LayoutInflater.from(c).inflate(
R.layout.list_item,
parent,
false
) as ConstraintLayout
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder) {
val itm = items[position]
val itm = items[position]
binding.title.text = itm.getTitleDecoded()
holder.mView.title.text = itm.getTitleDecoded()
binding.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
holder.mView.title.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
binding.title.setOnTouchListener(LinkOnTouchListener())
holder.mView.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(appColors.colorAccent)
holder.mView.title.setLinkTextColor(appColors.colorAccent)
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
holder.mView.sourceTitleAndDate.text = itm.sourceAndDateText()
binding.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
holder.mView.sourceTitleAndDate.setTextColor(ContextCompat.getColor(
c,
appColors.textColor
))
if (itm.getThumbnail(c).isEmpty()) {
if (itm.getThumbnail(c).isEmpty()) {
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
if (itm.getIcon(c).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle())
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
val drawable =
TextDrawable
.builder()
.round()
.build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.itemImage.setImageDrawable(drawable)
} else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
}
holder.mView.itemImage.setImageDrawable(drawable)
} else {
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
c.circularBitmapDrawable(config, itm.getIcon(c), holder.mView.itemImage)
}
} else {
c.bitmapCenterCrop(config, itm.getThumbnail(c), holder.mView.itemImage)
}
}
override fun getItemCount(): Int = items.size
inner class ViewHolder(val binding: ListItemBinding) : RecyclerView.ViewHolder(binding.root) {
inner class ViewHolder(val mView: ConstraintLayout) : RecyclerView.ViewHolder(mView) {
init {
handleCustomTabActions()
@ -101,11 +121,11 @@ class ItemListAdapter(
val customTabsIntent = c.buildCustomTabsIntent()
helper.bindCustomTabsService(app)
binding.root.setOnClickListener {
mView.setOnClickListener {
c.openItemUrl(
items,
bindingAdapterPosition,
items[bindingAdapterPosition].getLinkDecoded(),
adapterPosition,
items[adapterPosition].getLinkDecoded(),
customTabsIntent,
internalBrowser,
articleViewer,

View File

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

View File

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

View File

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

View File

@ -1,134 +0,0 @@
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

@ -48,19 +48,6 @@ data class Spout(
@SerializedName("description") val description: String
)
data class ApiVersion(
@SerializedName("version") val version: String?,
@SerializedName("apiversion") val apiversion: String?
) {
fun getApiMajorVersion() : Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
}
data class Source(
@SerializedName("id") val id: String,
@SerializedName("title") val title: String,
@ -88,7 +75,7 @@ data class Item(
@SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String,
@SerializedName("content") val content: String,
@SerializedName("unread") var unread: Boolean,
@SerializedName("unread") val unread: Boolean,
@SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?,
@ -169,7 +156,7 @@ data class Item(
fun preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
try {
@ -177,7 +164,7 @@ data class Item(
if ( URLUtil.isValidUrl(url)) {
val image = Glide.with(context).asBitmap()
.apply(glideOptions)
.load(url).submit()
.load(url).submit().get()
}
}
} catch (e : Error) {

View File

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

View File

@ -13,19 +13,16 @@ import androidx.work.Worker
import androidx.work.WorkerParameters
import apps.amine.bou.readerforselfoss.MainActivity
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.api.selfoss.getAndStoreAllItems
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.SharedItems
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAvailable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.persistence.toEntity
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -36,114 +33,103 @@ import kotlin.concurrent.thread
class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) {
lateinit var db: AppDatabase
override fun doWork(): Result {
val settings =
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
if (periodicRefresh) {
val api = SelfossApi(
this.context,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1")!!.toLong()
)
override fun doWork(): Result {
if (context.isNetworkAccessible(null)) {
if (isNetworkAvailable(context)) {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
CoroutineScope(Dispatchers.IO).launch {
val notificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
val notification =
NotificationCompat.Builder(applicationContext, Config.syncChannelId)
.setContentTitle(context.getString(R.string.loading_notification_title))
.setContentText(context.getString(R.string.loading_notification_text))
.setOngoing(true)
.setPriority(PRIORITY_LOW)
.setChannelId(Config.syncChannelId)
.setSmallIcon(R.drawable.ic_stat_cloud_download_black_24dp)
notificationManager.notify(1, notification.build())
notificationManager.notify(1, notification.build())
val settings =
this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false)
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3)
.addMigrations(MIGRATION_3_4).build()
val api = SelfossApi(
this.context,
null,
settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1")!!.toLong()
)
api.allItems().enqueue(object : Callback<List<Item>> {
override fun onFailure(call: Call<List<Item>>, t: Throwable) {
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
override fun onResponse(
call: Call<List<Item>>,
response: Response<List<Item>>
) {
thread {
if (response.body() != null) {
val apiItems = (response.body() as ArrayList<Item>)
db.itemsDao().deleteAllItems()
db.itemsDao()
.insertAllItems(*(apiItems.map { it.toEntity() }).toTypedArray())
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
val newItemsNotification = NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(context.getString(R.string.new_items_notification_text, newSize))
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
apiItems.map {it.preloadImages(context)}
}
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
}
})
thread {
val actions = db.actionsDao().actions()
actions.forEach { action ->
when {
action.read -> doAndReportOnFail(
api.markItem(action.articleId),
action
)
action.unread -> doAndReportOnFail(
api.unmarkItem(action.articleId),
action
)
action.starred -> doAndReportOnFail(
api.starrItem(action.articleId),
action
)
action.read -> doAndReportOnFail(api.markItem(action.articleId), action)
action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action)
action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action)
action.unstarred -> doAndReportOnFail(
api.unstarrItem(action.articleId),
action
)
}
}
getAndStoreAllItems(context, api, db)
SharedItems.updateDatabase(db)
storeItems(notifyNewItems, notificationManager)
}
}
}
return Result.success()
}
private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) {
CoroutineScope(Dispatchers.IO).launch {
val apiItems = SharedItems.items
val newSize = apiItems.filter { it.unread }.size
if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent: PendingIntent =
PendingIntent.getActivity(context, 0, intent, 0)
val newItemsNotification =
NotificationCompat.Builder(applicationContext, Config.newItemsChannelId)
.setContentTitle(context.getString(R.string.new_items_notification_title))
.setContentText(
context.getString(
R.string.new_items_notification_text,
newSize
)
)
.setPriority(PRIORITY_DEFAULT)
.setChannelId(Config.newItemsChannelId)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_tab_fiber_new_black_24dp)
Timer("", false).schedule(4000) {
notificationManager.notify(2, newItemsNotification.build())
}
}
apiItems.map { it.preloadImages(context) }
Timer("", false).schedule(4000) {
notificationManager.cancel(1)
}
}
return Result.success()
}
private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) {

View File

@ -14,7 +14,6 @@ import android.os.Bundle
import android.preference.PreferenceManager
import android.view.*
import android.webkit.*
import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment
@ -29,21 +28,29 @@ import apps.amine.bou.readerforselfoss.api.mercury.MercuryApi
import apps.amine.bou.readerforselfoss.api.mercury.ParsedContent
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.api.selfoss.SelfossApi
import apps.amine.bou.readerforselfoss.databinding.FragmentArticleBinding
import apps.amine.bou.readerforselfoss.api.selfoss.SuccessResponse
import apps.amine.bou.readerforselfoss.persistence.database.AppDatabase
import apps.amine.bou.readerforselfoss.persistence.entities.ActionEntity
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_1_2
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_2_3
import apps.amine.bou.readerforselfoss.persistence.migrations.MIGRATION_3_4
import apps.amine.bou.readerforselfoss.themes.AppColors
import apps.amine.bou.readerforselfoss.utils.*
import apps.amine.bou.readerforselfoss.utils.Config
import apps.amine.bou.readerforselfoss.utils.buildCustomTabsIntent
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import apps.amine.bou.readerforselfoss.utils.glide.loadMaybeBasicAuth
import apps.amine.bou.readerforselfoss.utils.glide.getBitmapInputStream
import apps.amine.bou.readerforselfoss.utils.isEmptyOrNullOrNullString
import apps.amine.bou.readerforselfoss.utils.network.isNetworkAccessible
import apps.amine.bou.readerforselfoss.utils.openItemUrl
import apps.amine.bou.readerforselfoss.utils.shareLink
import apps.amine.bou.readerforselfoss.utils.sourceAndDateText
import apps.amine.bou.readerforselfoss.utils.succeeded
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import kotlinx.android.synthetic.main.fragment_article.view.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
@ -51,6 +58,7 @@ import java.net.MalformedURLException
import java.net.URL
import java.util.concurrent.ExecutionException
import kotlin.collections.ArrayList
import kotlin.concurrent.thread
class ArticleFragment : Fragment() {
private lateinit var pageNumber: Number
@ -69,8 +77,8 @@ class ArticleFragment : Fragment() {
private lateinit var db: AppDatabase
private lateinit var textAlignment: String
private lateinit var config: Config
private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!!
private var rootView: ViewGroup? = null
private lateinit var prefs: SharedPreferences
@ -86,16 +94,16 @@ class ArticleFragment : Fragment() {
}
override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(requireActivity())
config = Config(requireActivity())
appColors = AppColors(activity!!)
config = Config(activity!!)
super.onCreate(savedInstanceState)
pageNumber = requireArguments().getInt(ARG_POSITION)
allItems = requireArguments().getParcelableArrayList<Item>(ARG_ITEMS) as ArrayList<Item>
pageNumber = arguments!!.getInt(ARG_POSITION)
allItems = arguments!!.getParcelableArrayList<Item>(ARG_ITEMS) as ArrayList<Item>
db = Room.databaseBuilder(
requireContext(),
context!!,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
}
@ -106,12 +114,13 @@ class ArticleFragment : Fragment() {
savedInstanceState: Bundle?
): View? {
try {
_binding = FragmentArticleBinding.inflate(inflater, container, false)
rootView = inflater
.inflate(R.layout.fragment_article, container, false) as ViewGroup
url = allItems[pageNumber.toInt()].getLinkDecoded()
contentText = allItems[pageNumber.toInt()].content
contentTitle = allItems[pageNumber.toInt()].getTitleDecoded()
contentImage = allItems[pageNumber.toInt()].getThumbnail(requireActivity())
contentImage = allItems[pageNumber.toInt()].getThumbnail(activity!!)
contentSource = allItems[pageNumber.toInt()].sourceAndDateText()
allImages = allItems[pageNumber.toInt()].getImages()
@ -121,11 +130,11 @@ class ArticleFragment : Fragment() {
font = prefs.getString("reader_font", "")!!
if (font.isNotEmpty()) {
resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
resId = context!!.resources.getIdentifier(font, "font", context!!.packageName)
typeface = try {
ResourcesCompat.getFont(requireContext(), resId)!!
ResourcesCompat.getFont(context!!, resId)!!
} catch (e: java.lang.Exception) {
// ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), requireContext())
// ACRA.getErrorReporter().maybeHandleSilentException(Throwable("Font loading issue: ${e.message}"), context!!)
// Just to be sure
null
}
@ -133,27 +142,27 @@ class ArticleFragment : Fragment() {
refreshAlignment()
val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val settings = activity!!.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val api = SelfossApi(
requireContext(),
requireActivity(),
context!!,
activity!!,
settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong()
)
fab = binding.fab
fab = rootView!!.fab
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
fab.rippleColor = appColors.colorAccentDark
val floatingToolbar: FloatingToolbar = binding.floatingToolbar
val floatingToolbar: FloatingToolbar = rootView!!.floatingToolbar
floatingToolbar.attachFab(fab)
floatingToolbar.background = ColorDrawable(appColors.colorAccent)
val customTabsIntent = requireActivity().buildCustomTabsIntent()
val customTabsIntent = activity!!.buildCustomTabsIntent()
mCustomTabActivityHelper = CustomTabActivityHelper()
mCustomTabActivityHelper!!.bindCustomTabsService(activity)
@ -163,43 +172,35 @@ class ArticleFragment : Fragment() {
override fun onItemClick(item: MenuItem) {
when (item.itemId) {
R.id.more_action -> getContentFromMercury(customTabsIntent, prefs)
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openItemUrl(
R.id.share_action -> activity!!.shareLink(url, contentTitle)
R.id.open_action -> activity!!.openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
false,
false,
requireActivity()
activity!!
)
R.id.unread_action -> if (context != null) {
if (allItems[pageNumber.toInt()].unread) {
SharedItems.readItem(
context!!,
api,
db,
allItems[pageNumber.toInt()]
)
allItems[pageNumber.toInt()].unread = false
Toast.makeText(
context,
R.string.marked_as_read,
Toast.LENGTH_LONG
).show()
} else {
SharedItems.unreadItem(
context!!,
api,
db,
allItems[pageNumber.toInt()]
)
allItems[pageNumber.toInt()].unread = true
Toast.makeText(
context,
R.string.marked_as_unread,
Toast.LENGTH_LONG
).show()
R.id.unread_action -> if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
api.unmarkItem(allItems[pageNumber.toInt()].id).enqueue(
object : Callback<SuccessResponse> {
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
}
override fun onFailure(
call: Call<SuccessResponse>,
t: Throwable
) {
}
}
)
} else {
thread {
db.actionsDao().insertAllActions(ActionEntity(allItems[pageNumber.toInt()].id, false, true, false, false))
}
}
else -> Unit
@ -211,35 +212,35 @@ class ArticleFragment : Fragment() {
}
)
binding.source.text = contentSource
rootView!!.source.text = contentSource
if (typeface != null) {
binding.source.typeface = typeface
rootView!!.source.typeface = typeface
}
if (contentText.isEmptyOrNullOrNullString()) {
getContentFromMercury(customTabsIntent, prefs)
} else {
binding.titleView.text = contentTitle
rootView!!.titleView.text = contentTitle
if (typeface != null) {
binding.titleView.typeface = typeface
rootView!!.titleView.typeface = typeface
}
htmlToWebview()
if (!contentImage.isEmptyOrNullOrNullString() && context != null) {
binding.imageView.visibility = View.VISIBLE
rootView!!.imageView.visibility = View.VISIBLE
Glide
.with(requireContext())
.with(context!!)
.asBitmap()
.loadMaybeBasicAuth(config, contentImage)
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
.into(rootView!!.imageView)
} else {
binding.imageView.visibility = View.GONE
rootView!!.imageView.visibility = View.GONE
}
}
binding.nestedScrollView.setOnScrollChangeListener(
rootView!!.nestedScrollView.setOnScrollChangeListener(
NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
if (scrollY > oldScrollY) {
fab.hide()
@ -250,27 +251,22 @@ class ArticleFragment : Fragment() {
)
} catch (e: InflateException) {
AlertDialog.Builder(requireContext())
.setMessage(requireContext().getString(R.string.webview_dialog_issue_message))
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
AlertDialog.Builder(context!!)
.setMessage(context!!.getString(R.string.webview_dialog_issue_message))
.setTitle(context!!.getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok
) { dialog, which ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext())
val sharedPref = PreferenceManager.getDefaultSharedPreferences(context!!)
val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false)
editor.commit()
requireActivity().finish()
activity!!.finish()
}
.create()
.show()
}
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
return rootView
}
private fun refreshAlignment() {
@ -285,8 +281,8 @@ class ArticleFragment : Fragment() {
customTabsIntent: CustomTabsIntent,
prefs: SharedPreferences
) {
if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) {
binding.progressBar.visibility = View.VISIBLE
if ((context != null && context!!.isNetworkAccessible(null)) || context == null) {
rootView!!.progressBar.visibility = View.VISIBLE
val parser = MercuryApi()
parser.parseUrl(url).enqueue(
@ -299,9 +295,9 @@ class ArticleFragment : Fragment() {
try {
if (response.body() != null && response.body()!!.content != null && !response.body()!!.content.isNullOrEmpty()) {
try {
binding.titleView.text = response.body()!!.title
rootView!!.titleView.text = response.body()!!.title
if (typeface != null) {
binding.titleView.typeface = typeface
rootView!!.titleView.typeface = typeface
}
try {
// Note: Mercury may return relative urls... If it does the url val will not be changed.
@ -321,18 +317,18 @@ class ArticleFragment : Fragment() {
try {
if (response.body()!!.lead_image_url != null && !response.body()!!.lead_image_url.isNullOrEmpty() && context != null) {
binding.imageView.visibility = View.VISIBLE
rootView!!.imageView.visibility = View.VISIBLE
try {
Glide
.with(requireContext())
.with(context!!)
.asBitmap()
.loadMaybeBasicAuth(config, response.body()!!.lead_image_url.orEmpty())
.apply(RequestOptions.fitCenterTransform())
.into(binding.imageView)
.into(rootView!!.imageView)
} catch (e: Exception) {
}
} else {
binding.imageView.visibility = View.GONE
rootView!!.imageView.visibility = View.GONE
}
} catch (e: Exception) {
if (context != null) {
@ -340,9 +336,9 @@ class ArticleFragment : Fragment() {
}
try {
binding.nestedScrollView.scrollTo(0, 0)
rootView!!.nestedScrollView.scrollTo(0, 0)
binding.progressBar.visibility = View.GONE
rootView!!.progressBar.visibility = View.GONE
} catch (e: Exception) {
if (context != null) {
}
@ -374,32 +370,32 @@ class ArticleFragment : Fragment() {
val stringColor = String.format("#%06X", 0xFFFFFF and appColors.colorAccent)
val attrs: IntArray = intArrayOf(android.R.attr.fontFamily)
val a: TypedArray = requireContext().obtainStyledAttributes(resId, attrs)
val a: TypedArray = context!!.obtainStyledAttributes(resId, attrs)
binding.webcontent.settings.standardFontFamily = a.getString(0)
binding.webcontent.visibility = View.VISIBLE
rootView!!.webcontent.settings.standardFontFamily = a.getString(0)
rootView!!.webcontent.visibility = View.VISIBLE
val (textColor, backgroundColor) = if (appColors.isDarkTheme) {
if (context != null) {
binding.webcontent.setBackgroundColor(
rootView!!.webcontent.setBackgroundColor(
ContextCompat.getColor(
requireContext(),
context!!,
R.color.dark_webview
)
)
Pair(ContextCompat.getColor(requireContext(), R.color.dark_webview_text), ContextCompat.getColor(requireContext(), R.color.dark_webview))
Pair(ContextCompat.getColor(context!!, R.color.dark_webview_text), ContextCompat.getColor(context!!, R.color.dark_webview))
} else {
Pair(null, null)
}
} else {
if (context != null) {
binding.webcontent.setBackgroundColor(
rootView!!.webcontent.setBackgroundColor(
ContextCompat.getColor(
requireContext(),
context!!,
R.color.light_webview
)
)
Pair(ContextCompat.getColor(requireContext(), R.color.light_webview_text), ContextCompat.getColor(requireContext(), R.color.light_webview))
Pair(ContextCompat.getColor(context!!, R.color.light_webview_text), ContextCompat.getColor(context!!, R.color.light_webview))
} else {
Pair(null, null)
}
@ -417,14 +413,14 @@ class ArticleFragment : Fragment() {
"#FFFFFF"
}
binding.webcontent.settings.useWideViewPort = true
binding.webcontent.settings.loadWithOverviewMode = true
binding.webcontent.settings.javaScriptEnabled = false
rootView!!.webcontent.settings.useWideViewPort = true
rootView!!.webcontent.settings.loadWithOverviewMode = true
rootView!!.webcontent.settings.javaScriptEnabled = false
binding.webcontent.webViewClient = object : WebViewClient() {
rootView!!.webcontent.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url : String): Boolean {
if (binding.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
requireContext().startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
if (rootView!!.webcontent.hitTestResult.type != WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
rootView!!.context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
return true
}
@ -460,13 +456,13 @@ class ArticleFragment : Fragment() {
}
})
binding.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
rootView!!.webcontent.setOnTouchListener { _, event -> gestureDetector.onTouchEvent(event)}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
binding.webcontent.settings.layoutAlgorithm =
rootView!!.webcontent.settings.layoutAlgorithm =
WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING
} else {
binding.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
rootView!!.webcontent.settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.SINGLE_COLUMN
}
var baseUrl: String? = null
@ -495,7 +491,7 @@ class ArticleFragment : Fragment() {
""
}
binding.webcontent.loadDataWithBaseURL(
rootView!!.webcontent.loadDataWithBaseURL(
baseUrl,
"""<html>
|<head>
@ -548,15 +544,15 @@ class ArticleFragment : Fragment() {
}
private fun openInBrowserAfterFailing(customTabsIntent: CustomTabsIntent) {
binding.progressBar.visibility = View.GONE
requireActivity().openItemUrl(
rootView!!.progressBar.visibility = View.GONE
activity!!.openItemUrl(
allItems,
pageNumber.toInt(),
url,
customTabsIntent,
true,
false,
requireActivity()
activity!!
)
}
@ -578,10 +574,10 @@ class ArticleFragment : Fragment() {
}
fun performClick(): Boolean {
if (binding.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
binding.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
if (rootView!!.webcontent.hitTestResult.type == WebView.HitTestResult.IMAGE_TYPE ||
rootView!!.webcontent.hitTestResult.type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
val position : Int = allImages.indexOf(binding.webcontent.hitTestResult.extra)
val position : Int = allImages.indexOf(rootView!!.webcontent.hitTestResult.extra)
val intent = Intent(activity, ImageActivity::class.java)
intent.putExtra("allImages", allImages)

View File

@ -4,17 +4,15 @@ import android.os.Bundle
import android.view.*
import androidx.fragment.app.Fragment
import apps.amine.bou.readerforselfoss.R
import apps.amine.bou.readerforselfoss.databinding.FragmentImageBinding
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import kotlinx.android.synthetic.main.fragment_image.view.*
class ImageFragment : Fragment() {
private lateinit var imageUrl : String
private val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL)
private var _binding: FragmentImageBinding? = null
private val binding get() = _binding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -23,24 +21,18 @@ class ImageFragment : Fragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentImageBinding.inflate(inflater, container, false)
val view = binding?.root
val view : View = inflater.inflate(R.layout.fragment_image, container, false)
binding!!.photoView.visibility = View.VISIBLE
view.photoView.visibility = View.VISIBLE
Glide.with(activity)
.asBitmap()
.apply(glideOptions)
.load(imageUrl)
.into(binding!!.photoView)
.into(view.photoView)
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
private const val ARG_IMAGE = "imageUrl"

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,6 @@ import apps.amine.bou.readerforselfoss.ReaderActivity
import apps.amine.bou.readerforselfoss.api.selfoss.Item
import apps.amine.bou.readerforselfoss.utils.customtabs.CustomTabActivityHelper
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
@ -75,7 +74,6 @@ fun Context.openItemUrlInternally(
) {
if (articleViewer) {
ReaderActivity.allItems = allItems
SharedItems.position = currentItem
val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem)
app.startActivity(intent)
@ -139,13 +137,13 @@ private fun openInBrowser(linkDecoded: String, app: Activity) {
}
fun String.isUrlValid(): Boolean =
this.toHttpUrlOrNull() != null && Patterns.WEB_URL.matcher(this).matches()
HttpUrl.parse(this) != null && Patterns.WEB_URL.matcher(this).matches()
fun String.isBaseUrlValid(ctx: Context): Boolean {
val baseUrl = this.toHttpUrlOrNull()
val baseUrl = HttpUrl.parse(this)
var existsAndEndsWithSlash = false
if (baseUrl != null) {
val pathSegments = baseUrl.pathSegments
val pathSegments = baseUrl.pathSegments()
existsAndEndsWithSlash = "" == pathSegments[pathSegments.size - 1]
}

View File

@ -1,318 +0,0 @@
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))
}
}
}
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

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

View File

@ -1,5 +0,0 @@
<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

@ -0,0 +1,5 @@
<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

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

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Els articles no es guardaran a la memòria del dispositiu i l\'aplicació no es podrà utilitzar sense connexió.</string>
<string name="pref_switch_items_caching_on">Els articles es guardaran a la memòria del dispositiu i es podran utilitzar sense connexió.</string>
<string name="pref_switch_items_caching">Guarda els elements per utilitzar-los sense connexió</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Sense connexió!</string>
<string name="pref_switch_periodic_refresh">Sincronitza els articles</string>
<string name="pref_switch_periodic_refresh_off">Els articles no se sincronitzaran en segon pla</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Nicht verbunden !</string>
<string name="pref_switch_periodic_refresh">Synchronisiere Artikel</string>
<string name="pref_switch_periodic_refresh_off">Artikel werden nicht im Hintergrund synchronisiert</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Los artículos no se guardarán en la memoria del dispositivo y la aplicación no se podrá utilizar sin conexión.</string>
<string name="pref_switch_items_caching_on">Los artículos se guardarán en la memoria del dispositivo y se utilizarán para el uso sin conexión.</string>
<string name="pref_switch_items_caching">Guardar elementos para uso sin conexión</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Sin conexión!</string>
<string name="pref_switch_periodic_refresh">Sincronizar artículos</string>
<string name="pref_switch_periodic_refresh_off">Los artículos no se sincronizarán en segundo plano</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<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">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_summary">Disable this if your server is receiving excessive amounts of database queries.</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_off">Les articles ne seront pas synchronisés en arrière plan</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Os artigos non se gardaran na memoria do dispositivo e non se poderá utilizar a aplicación sen conexión.</string>
<string name="pref_switch_items_caching_on">Os artigos gardaranse na memoria do dispositivo e estarán dispoñibles sen conexión.</string>
<string name="pref_switch_items_caching">Gardar elementos para uso sen conexión</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Non conectado!</string>
<string name="pref_switch_periodic_refresh">Sincronizar artigos</string>
<string name="pref_switch_periodic_refresh_off">Os artigos non se sincronizarán coa aplicación de fondo</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">文章不会被保存到设备内存,应用程序在离线时将无法阅读它们</string>
<string name="pref_switch_items_caching_on">文章将被保存到设备内存并可在离线时使用</string>
<string name="pref_switch_items_caching">保存项目以便离线使用</string>
<string name="pref_switch_update_sources">检查新来源和标签</string>
<string name="pref_switch_update_sources_summary">如果你的服务器接收过多的数据库查询,请禁用此功能。</string>
<string name="no_network_connectivity">未连接!</string>
<string name="pref_switch_periodic_refresh">同步文章</string>
<string name="pref_switch_periodic_refresh_off">文章将不会在后台同步</string>

View File

@ -139,8 +139,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -140,8 +140,6 @@
<string name="pref_switch_items_caching_off">Articles won\'t be saved to the device memory, and the app won\'t be usable offline.</string>
<string name="pref_switch_items_caching_on">Articles will be saved to the device memory and will be used for offline use.</string>
<string name="pref_switch_items_caching">Save items for offline use</string>
<string name="pref_switch_update_sources">Check for new sources and tags</string>
<string name="pref_switch_update_sources_summary">Disable this if your server is receiving excessive amounts of database queries.</string>
<string name="no_network_connectivity">Not connected !</string>
<string name="pref_switch_periodic_refresh">Sync articles</string>
<string name="pref_switch_periodic_refresh_off">Articles will not be synced in the background</string>

View File

@ -34,10 +34,4 @@
android:key="notify_new_items"
android:dependency="periodic_refresh"
android:title="@string/pref_switch_notify_new_items" />
<SwitchPreference
android:defaultValue="true"
android:key="update_sources"
android:summary="@string/pref_switch_update_sources_summary"
android:title="@string/pref_switch_update_sources" />
</PreferenceScreen>

View File

@ -2,7 +2,7 @@
buildscript {
ext {
kotlin_version = '1.5.31'
kotlin_version = '1.4.21'
android_version = '1.0.0'
androidx_version = '1.1.0-alpha05'
lifecycle_version = '2.2.0-alpha01'
@ -17,7 +17,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -37,4 +37,14 @@ task clean(type: Delete) {
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

@ -1,13 +0,0 @@
A new RSS reader for <a href="http://selfoss.aditu.de/">selfoss</a>.
What it does:
<ul>
<li>Fetches read, unread, and favorite feeds.</li>
<li>Marking as read, marking as favorite.</li>
<li>Manage selfoss sources from the app.</li>
<li>Add an RSS feed from within the app, or by sharing a link from your browser.</li>
<li>Choose between multiple light and dark themes.</li>
</ul>
PS: It only works with Selfoss

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

View File

@ -1,3 +0,0 @@
A new RSS reader for <a href="http://selfoss.aditu.de/">selfoss</a>.
It connects to your selfoss instance (works only with selfoss, and can't work without it), and you'll be able to read and manage all your RSS feeds.

View File

@ -1 +0,0 @@
Reader for Selfoss

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 422 KiB

After

Width:  |  Height:  |  Size: 422 KiB