WIP. Everything isn't working yet, but api calls are made.

This commit is contained in:
aminecmi 2022-05-21 10:41:09 +02:00
parent bd4d20e858
commit 6e38e8753c
47 changed files with 1427 additions and 2112 deletions

5
BREAKING.md Normal file
View File

@ -0,0 +1,5 @@
# Breaking changes
These breaking changes will **NOT** be handled in this version.
- Basic (kinda) and Digest auth. This version will use the selfoss credentials via Basic authentication.

10
TODO.md Normal file
View File

@ -0,0 +1,10 @@
# TODO
- Service injection
- Basic auth
* Self signed certs
* Timeout + 408
- Clean HTTP login

View File

@ -14,9 +14,6 @@ import android.widget.ProgressBar
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Spout
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SuccessResponse
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
@ -26,10 +23,19 @@ import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class AddSourceActivity : AppCompatActivity() { class AddSourceActivity : AppCompatActivity() {
private lateinit var apiDetailsService: ApiDetailsService
private var mSpoutsValue: String? = null private var mSpoutsValue: String? = null
private lateinit var api: SelfossApi private lateinit var api: SelfossApi
@ -74,11 +80,13 @@ class AddSourceActivity : AppCompatActivity() {
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val settings = val settings =
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
apiDetailsService = AndroidApiDetailsService(this@AddSourceActivity)
api = SelfossApi( api = SelfossApi(
this, // this,
this@AddSourceActivity, // this@AddSourceActivity,
settings.getBoolean("isSelfSignedCert", false), // settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong() // prefs.getString("api_timeout", "-1")!!.toLong()
apiDetailsService
) )
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
mustLoginToAddSource() mustLoginToAddSource()
@ -124,41 +132,28 @@ class AddSourceActivity : AppCompatActivity() {
} }
} }
var items: Map<String, Spout>
api!!.spouts().enqueue(object : Callback<Map<String, Spout>> {
override fun onResponse(
call: Call<Map<String, Spout>>,
response: Response<Map<String, Spout>>
) {
if (response.body() != null) {
items = response.body()!!
val itemsStrings = items.map { it.value.name } CoroutineScope(Dispatchers.IO).launch {
for ((key, value) in items) { var items = api!!.spouts()
spoutsKV[value.name] = key if (items != null) {
}
mProgress.visibility = View.GONE val itemsStrings = items.map { it.value.name }
formContainer.visibility = View.VISIBLE for ((key, value) in items) {
spoutsKV[value.name] = key
val spinnerArrayAdapter =
ArrayAdapter(
this@AddSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spoutsSpinner.adapter = spinnerArrayAdapter
} else {
handleProblemWithSpouts()
} }
}
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) { mProgress.visibility = View.GONE
handleProblemWithSpouts() formContainer.visibility = View.VISIBLE
}
private fun handleProblemWithSpouts() { val spinnerArrayAdapter =
ArrayAdapter(
this@AddSourceActivity,
android.R.layout.simple_spinner_item,
itemsStrings
)
spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spoutsSpinner.adapter = spinnerArrayAdapter
} else {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@AddSourceActivity,
R.string.cant_get_spouts, R.string.cant_get_spouts,
@ -166,7 +161,7 @@ class AddSourceActivity : AppCompatActivity() {
).show() ).show()
mProgress.visibility = View.GONE mProgress.visibility = View.GONE
} }
}) }
} }
private fun maybeGetDetailsFromIntentSharing( private fun maybeGetDetailsFromIntentSharing(
@ -196,70 +191,26 @@ class AddSourceActivity : AppCompatActivity() {
sourceDetailsUnavailable -> { sourceDetailsUnavailable -> {
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show() 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) {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
})
}
else -> { else -> {
api.createSource( CoroutineScope(Dispatchers.IO).launch {
title, val response: SelfossModel.SuccessResponse? = api.createSourceForVersion(
url, title,
mSpoutsValue!!, url,
tags.text.toString(), mSpoutsValue!!,
"" tags.text.toString(),
).enqueue(object : Callback<SuccessResponse> { "",
override fun onResponse( PreferenceManager.getDefaultSharedPreferences(this@AddSourceActivity).getInt("apiVersionMajor", 0)
call: Call<SuccessResponse>, )
response: Response<SuccessResponse> if (response != null) {
) { finish()
if (response.body() != null && response.body()!!.isSuccess) { } else {
finish()
} else {
Toast.makeText(
this@AddSourceActivity,
R.string.cant_create_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@AddSourceActivity, this@AddSourceActivity,
R.string.cant_create_source, R.string.cant_create_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) }
} }
} }
} }

View File

@ -29,27 +29,37 @@ import androidx.work.WorkManager
import bou.amine.apps.readerforselfossv2.android.adapters.ItemCardAdapter import bou.amine.apps.readerforselfossv2.android.adapters.ItemCardAdapter
import bou.amine.apps.readerforselfossv2.android.adapters.ItemListAdapter import bou.amine.apps.readerforselfossv2.android.adapters.ItemListAdapter
import bou.amine.apps.readerforselfossv2.android.adapters.ItemsAdapter import bou.amine.apps.readerforselfossv2.android.adapters.ItemsAdapter
import bou.amine.apps.readerforselfossv2.android.api.selfoss.*
import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker import bou.amine.apps.readerforselfossv2.android.background.LoadingWorker
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityHomeBinding
import bou.amine.apps.readerforselfossv2.android.model.getIcon
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabaseService
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.SharedItems
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.longHash import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView
import bou.amine.apps.readerforselfossv2.android.api.selfoss.*
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.service.SelfossService
import bou.amine.apps.readerforselfossv2.utils.longHash
import com.ashokvarma.bottomnavigation.BottomNavigationBar import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem import com.ashokvarma.bottomnavigation.TextBadgeItem
@ -73,14 +83,16 @@ import com.mikepenz.materialdrawer.widget.AccountHeaderView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread import kotlin.concurrent.thread
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private lateinit var dataBase: AndroidDeviceDatabase
private lateinit var dbService: AndroidDeviceDatabaseService
private lateinit var searchService: SearchService
private lateinit var apiDetailsService: ApiDetailsService
private lateinit var service: SelfossService<AndroidItemEntity>
private val MENU_PREFERENCES = 12302 private val MENU_PREFERENCES = 12302
private val DRAWER_ID_TAGS = 100101L private val DRAWER_ID_TAGS = 100101L
private val DRAWER_ID_HIDDEN_TAGS = 101100L private val DRAWER_ID_HIDDEN_TAGS = 101100L
@ -90,8 +102,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private val READ_SHOWN = 2 private val READ_SHOWN = 2
private val FAV_SHOWN = 3 private val FAV_SHOWN = 3
private var items: ArrayList<Item> = ArrayList() private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var allItems: ArrayList<Item> = ArrayList() private var allItems: ArrayList<SelfossModel.Item> = ArrayList()
private var internalBrowser = false private var internalBrowser = false
private var articleViewer = false private var articleViewer = false
@ -105,7 +117,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var displayAccountHeader: Boolean = false private var displayAccountHeader: Boolean = false
private var infiniteScroll: Boolean = false private var infiniteScroll: Boolean = false
private var lastFetchDone: Boolean = false private var lastFetchDone: Boolean = false
private var itemsCaching: Boolean = false
private var updateSources: Boolean = true private var updateSources: Boolean = true
private var markOnScroll: Boolean = false private var markOnScroll: Boolean = false
private var hiddenTags: List<String> = emptyList() private var hiddenTags: List<String> = emptyList()
@ -140,7 +151,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private lateinit var config: Config private lateinit var config: Config
data class DrawerData(val tags: List<Tag>?, val sources: List<Source>?) data class DrawerData(val tags: List<SelfossModel.Tag>?, val sources: List<SelfossModel.Source>?)
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@ -184,12 +195,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
sharedPref = PreferenceManager.getDefaultSharedPreferences(this) sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
apiDetailsService = AndroidApiDetailsService(applicationContext)
api = SelfossApi( api = SelfossApi(
this, // this,
this@HomeActivity, // this@HomeActivity,
settings.getBoolean("isSelfSignedCert", false), // settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1")!!.toLong() // sharedPref.getString("api_timeout", "-1")!!.toLong()
apiDetailsService
) )
dataBase = AndroidDeviceDatabase(applicationContext)
searchService = SearchService(DateUtils(apiDetailsService))
dbService = AndroidDeviceDatabaseService(dataBase, searchService)
service = SelfossService(api, dbService, searchService)
items = ArrayList() items = ArrayList()
allItems = ArrayList() allItems = ArrayList()
@ -217,7 +235,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
lastFetchDone = false lastFetchDone = false
handleDrawerItems() handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
refreshFocusedItems(applicationContext, api, db, itemsNumber) service.refreshFocusedItems(itemsNumber, applicationContext.isNetworkAvailable())
getElementsAccordingToTab() getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
} }
@ -258,7 +276,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
reloadBadgeContent() reloadBadgeContent()
val tagHashes = i.tags.tags.split(",").map { it.longHash() } val tagHashes = i.tags.split(",").map { it.longHash() }
tagsBadge = tagsBadge.map { tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) { if (tagHashes.contains(it.key)) {
(it.key to (it.value - 1)) (it.key to (it.value - 1))
@ -334,21 +352,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
private fun getApiMajorVersion() { private fun getApiMajorVersion() {
api.apiVersion.enqueue(object : Callback<ApiVersion> { CoroutineScope(Dispatchers.IO).launch {
override fun onFailure(call: Call<ApiVersion>, t: Throwable) { val version = api.version()
Config.apiVersion = apiVersionMajor if (version != null) {
apiVersionMajor = version.getApiMajorVersion()
sharedPref.edit().putInt("apiVersionMajor", apiVersionMajor).apply()
} }
}
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).apply()
Config.apiVersion = apiVersionMajor
}
}
})
} }
override fun onResume() { override fun onResume() {
@ -382,17 +392,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
getElementsAccordingToTab() 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)
}
}
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
customTabActivityHelper.unbindCustomTabsService(this) customTabActivityHelper.unbindCustomTabsService(this)
@ -409,8 +408,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
userIdentifier = sharedPref.getString("unique_id", "")!! userIdentifier = sharedPref.getString("unique_id", "")!!
displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false) displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false)
infiniteScroll = sharedPref.getBoolean("infinite_loading", false) infiniteScroll = sharedPref.getBoolean("infinite_loading", false)
itemsCaching = sharedPref.getBoolean("items_caching", false) searchService.itemsCaching = sharedPref.getBoolean("items_caching", false)
SharedItems.itemsCaching = itemsCaching
updateSources = sharedPref.getBoolean("update_sources", true) updateSources = sharedPref.getBoolean("update_sources", true)
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false) markOnScroll = sharedPref.getBoolean("mark_on_scroll", false)
hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) { hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) {
@ -525,7 +523,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun handleDrawerItems() { private fun handleDrawerItems() {
tagsBadge = emptyMap() tagsBadge = emptyMap()
fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) { fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) {
fun handleTags(maybeTags: List<Tag>?) { fun handleTags(maybeTags: List<SelfossModel.Tag>?) {
if (maybeTags == null) { if (maybeTags == null) {
if (loadedFromCache) { if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add( binding.mainDrawer.itemAdapter.add(
@ -560,9 +558,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
color = ColorHolder.fromColor(appColors.colorAccent) } color = ColorHolder.fromColor(appColors.colorAccent) }
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() allItems = ArrayList()
SharedItems.tagFilter = it.tag searchService.tagFilter = it.tag
SharedItems.sourceFilter = null searchService.sourceFilter = null
SharedItems.sourceIDFilter = null searchService.sourceIDFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
false false
@ -578,7 +576,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
} }
fun handleHiddenTags(maybeTags: List<Tag>?) { fun handleHiddenTags(maybeTags: List<SelfossModel.Tag>?) {
if (maybeTags == null) { if (maybeTags == null) {
if (loadedFromCache) { if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add( binding.mainDrawer.itemAdapter.add(
@ -589,7 +587,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
) )
} }
} else { } else {
val filteredHiddenTags: List<Tag> = val filteredHiddenTags: List<SelfossModel.Tag> =
maybeTags.filter { hiddenTags.contains(it.tag) } maybeTags.filter { hiddenTags.contains(it.tag) }
tagsBadge = filteredHiddenTags.map { tagsBadge = filteredHiddenTags.map {
val gd = GradientDrawable() val gd = GradientDrawable()
@ -613,9 +611,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
color = ColorHolder.fromColor(appColors.colorAccent) } color = ColorHolder.fromColor(appColors.colorAccent) }
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() allItems = ArrayList()
SharedItems.tagFilter = it.tag searchService.tagFilter = it.tag
SharedItems.sourceFilter = null searchService.sourceFilter = null
SharedItems.sourceIDFilter = null searchService.sourceIDFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
false false
@ -631,7 +629,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
} }
fun handleSources(maybeSources: List<Source>?) { fun handleSources(maybeSources: List<SelfossModel.Source>?) {
if (maybeSources == null) { if (maybeSources == null) {
if (loadedFromCache) { if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add( binding.mainDrawer.itemAdapter.add(
@ -646,12 +644,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
val item = PrimaryDrawerItem().apply { val item = PrimaryDrawerItem().apply {
nameText = source.getTitleDecoded() nameText = source.getTitleDecoded()
identifier = source.id.toLong() identifier = source.id.toLong()
iconUrl = source.getIcon(this@HomeActivity) iconUrl = source.getIcon(apiDetailsService.getBaseUrl())
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() allItems = ArrayList()
SharedItems.sourceIDFilter = source.id.toLong() searchService.sourceIDFilter = source.id.toLong()
SharedItems.sourceFilter = source.title searchService.sourceFilter = source.title
SharedItems.tagFilter = null searchService.tagFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
false false
@ -672,9 +670,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
badgeRes = R.string.drawer_action_clear badgeRes = R.string.drawer_action_clear
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() allItems = ArrayList()
SharedItems.sourceFilter = null searchService.sourceFilter = null
SharedItems.sourceIDFilter = null searchService.sourceIDFilter = null
SharedItems.tagFilter = null searchService.tagFilter = null
binding.mainDrawer.setSelectionAtPosition(-1) binding.mainDrawer.setSelectionAtPosition(-1)
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
@ -769,47 +767,37 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
fun drawerApiCalls(maybeDrawerData: DrawerData?) { fun drawerApiCalls(maybeDrawerData: DrawerData?) {
var tags: List<Tag>? = null var tags: List<SelfossModel.Tag>? = null
var sources: List<Source>? var sources: List<SelfossModel.Source>?
fun sourcesApiCall() { fun sourcesApiCall() {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
api.sources.enqueue(object : Callback<List<Source>> { CoroutineScope(Dispatchers.Main).launch {
override fun onResponse( val response = api.sources()
call: Call<List<Source>>?, if (response != null) {
response: Response<List<Source>> sources = response
) {
sources = response.body()
val apiDrawerData = DrawerData(tags, sources) val apiDrawerData = DrawerData(tags, sources)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) { if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData) handleDrawerData(apiDrawerData)
} }
} } else {
override fun onFailure(call: Call<List<Source>>?, t: Throwable?) {
val apiDrawerData = DrawerData(tags, null) val apiDrawerData = DrawerData(tags, null)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) { if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData) handleDrawerData(apiDrawerData)
} }
} }
}) }
} }
} }
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
api.tags.enqueue(object : Callback<List<Tag>> { CoroutineScope(Dispatchers.IO).launch {
override fun onResponse( val response = api.tags()
call: Call<List<Tag>>, if (response != null) {
response: Response<List<Tag>> tags = response
) {
tags = response.body()
sourcesApiCall()
} }
sourcesApiCall()
override fun onFailure(call: Call<List<Tag>>?, t: Throwable?) { }
sourcesApiCall()
}
})
} }
} }
@ -914,9 +902,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun fetchOnEmptyList() { private fun fetchOnEmptyList() {
binding.recyclerView.doOnNextLayout { binding.recyclerView.doOnNextLayout {
if (SharedItems.focusedItems.size - 1 == getLastVisibleItem()) { // Todo:
getElementsAccordingToTab(true) // if (SharedItems.focusedItems.size - 1 == getLastVisibleItem()) {
} // getElementsAccordingToTab(true)
// }
} }
} }
@ -968,11 +957,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
} }
offset = if (appendResults) { // Todo:
SharedItems.focusedItems.size - 1 // offset = if (appendResults) {
} else { // SharedItems.focusedItems.size - 1
0 // } else {
} // 0
// }
firstVisible = if (appendResults) firstVisible else 0 firstVisible = if (appendResults) firstVisible else 0
doGetAccordingToTab() doGetAccordingToTab()
@ -980,39 +970,41 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun getUnRead(appendResults: Boolean = false) { private fun getUnRead(appendResults: Boolean = false) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (appendResults || !SharedItems.fetchedUnread) { // Todo:
binding.swipeRefreshLayout.isRefreshing = true // if (appendResults || !SharedItems.fetchedUnread) {
getUnreadItems(applicationContext, api, db, itemsNumber, offset) // binding.swipeRefreshLayout.isRefreshing = true
binding.swipeRefreshLayout.isRefreshing = false // service.getUnreadItems(itemsNumber, offset, applicationContext.isNetworkAvailable())
} // binding.swipeRefreshLayout.isRefreshing = false
SharedItems.getUnRead() // }
items = SharedItems.focusedItems // Todo: SharedItems.getUnRead()
// Todo: items = SharedItems.focusedItems
handleListResult() handleListResult()
} }
} }
private fun getRead(appendResults: Boolean = false) { private fun getRead(appendResults: Boolean = false) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (appendResults || !SharedItems.fetchedAll) { // Todo:
binding.swipeRefreshLayout.isRefreshing = true // if (appendResults || !SharedItems.fetchedAll) {
getReadItems(applicationContext, api, db, itemsNumber, offset) // binding.swipeRefreshLayout.isRefreshing = true
binding.swipeRefreshLayout.isRefreshing = false // service.getReadItems(itemsNumber, offset, applicationContext.isNetworkAvailable())
} // binding.swipeRefreshLayout.isRefreshing = false
SharedItems.getAll() // }
items = SharedItems.focusedItems // SharedItems.getAll()
// items = SharedItems.focusedItems
handleListResult() handleListResult()
} }
} }
private fun getStarred(appendResults: Boolean = false) { private fun getStarred(appendResults: Boolean = false) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
if (appendResults || !SharedItems.fetchedStarred) { if (appendResults || !searchService.fetchedStarred) {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
getStarredItems(applicationContext, api, db, itemsNumber, offset) service.getStarredItems(itemsNumber, offset, applicationContext.isNetworkAvailable())
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
} }
SharedItems.getStarred() // Todo: SharedItems.getStarred()
items = SharedItems.focusedItems // Todo: items = SharedItems.focusedItems
handleListResult() handleListResult()
} }
} }
@ -1036,6 +1028,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this, this,
items, items,
api, api,
apiDetailsService,
db, db,
customTabActivityHelper, customTabActivityHelper,
internalBrowser, internalBrowser,
@ -1043,7 +1036,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
fullHeightCards, fullHeightCards,
appColors, appColors,
userIdentifier, userIdentifier,
config config,
searchService
) { ) {
updateItems(it) updateItems(it)
} }
@ -1053,13 +1047,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
this, this,
items, items,
api, api,
apiDetailsService,
db, db,
customTabActivityHelper, customTabActivityHelper,
internalBrowser, internalBrowser,
articleViewer, articleViewer,
userIdentifier, userIdentifier,
appColors, appColors,
config config,
searchService
) { ) {
updateItems(it) updateItems(it)
} }
@ -1083,7 +1079,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun reloadBadges() { private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) { if (displayUnreadCount || displayAllCount) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
reloadBadges(applicationContext, api) service.reloadBadges(applicationContext.isNetworkAvailable())
reloadBadgeContent() reloadBadgeContent()
} }
} }
@ -1092,15 +1088,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun reloadBadgeContent() { private fun reloadBadgeContent() {
if (displayUnreadCount) { if (displayUnreadCount) {
tabNewBadge tabNewBadge
.setText(SharedItems.badgeUnread.toString()) .setText(searchService.badgeUnread.toString())
.maybeShow() .maybeShow()
} }
if (displayAllCount) { if (displayAllCount) {
tabArchiveBadge tabArchiveBadge
.setText(SharedItems.badgeAll.toString()) .setText(searchService.badgeAll.toString())
.maybeShow() .maybeShow()
tabStarredBadge tabStarredBadge
.setText(SharedItems.badgeStarred.toString()) .setText(searchService.badgeStarred.toString())
.maybeShow() .maybeShow()
} }
} }
@ -1120,7 +1116,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onQueryTextChange(p0: String?): Boolean { override fun onQueryTextChange(p0: String?): Boolean {
if (p0.isNullOrBlank()) { if (p0.isNullOrBlank()) {
SharedItems.searchFilter = null searchService.searchFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
} }
@ -1128,7 +1124,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
override fun onQueryTextSubmit(p0: String?): Boolean { override fun onQueryTextSubmit(p0: String?): Boolean {
SharedItems.searchFilter = p0 searchService.searchFilter = p0
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
return false return false
@ -1158,29 +1154,25 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.refresh -> { R.id.refresh -> {
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
api.update().enqueue(object : Callback<String> { Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
override fun onResponse( CoroutineScope(Dispatchers.Main).launch {
call: Call<String>, val status = api.update()
response: Response<String> if (status != null && status.isSuccess) {
) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG R.string.refresh_success_response, Toast.LENGTH_LONG
) )
.show() .show()
} } else {
override fun onFailure(call: Call<String>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_failer_message, R.string.refresh_failer_message,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) }
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
} }
return true return true
} else { } else {
@ -1192,9 +1184,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val success = readAll(applicationContext, api, db) val success = service.readAll(applicationContext.isNetworkAvailable())
if (success) { if (success) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
@ -1230,13 +1222,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun maxItemNumber(): Int = private fun maxItemNumber(): Int =
when (elementsShown) { when (elementsShown) {
UNREAD_SHOWN -> SharedItems.badgeUnread UNREAD_SHOWN -> searchService.badgeUnread
READ_SHOWN -> SharedItems.badgeAll READ_SHOWN -> searchService.badgeAll
FAV_SHOWN -> SharedItems.badgeStarred FAV_SHOWN -> searchService.badgeStarred
else -> SharedItems.badgeUnread // if !elementsShown then unread are fetched. else -> searchService.badgeUnread // if !elementsShown then unread are fetched.
} }
private fun updateItems(adapterItems: ArrayList<Item>) { private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
items = adapterItems items = adapterItems
} }
@ -1259,32 +1251,24 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
private fun handleOfflineActions() { private fun handleOfflineActions() {
fun <T>doAndReportOnFail(call: Call<T>, action: ActionEntity) { fun doAndReportOnFail(call: SelfossModel.SuccessResponse?, action: ActionEntity) {
call.enqueue(object: Callback<T> { if (call != null && call.isSuccess) {
override fun onResponse( thread {
call: Call<T>, db.actionsDao().delete(action)
response: Response<T> }
) { }
thread {
db.actionsDao().delete(action)
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
}
})
} }
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val actions = db.actionsDao().actions() val actions = db.actionsDao().actions()
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail(api.markItem(action.articleId), action) action.read -> doAndReportOnFail(api.markAsRead(action.articleId), action)
action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action) action.unread -> doAndReportOnFail(api.unmarkAsRead(action.articleId), action)
action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action) action.starred -> doAndReportOnFail(api.starr(action.articleId), action)
action.unstarred -> doAndReportOnFail(api.unstarrItem(action.articleId), action) action.unstarred -> doAndReportOnFail(api.unstarr(action.articleId), action)
} }
} }
} }

View File

@ -8,19 +8,26 @@ import android.os.Bundle
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView import android.widget.TextView
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi import androidx.work.Logger
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SuccessResponse
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -120,6 +127,21 @@ class LoginActivity : AppCompatActivity() {
finish() finish()
} }
private fun preferenceError(t: Throwable) {
editor.remove("url")
editor.remove("login")
editor.remove("httpUserName")
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)
showProgress(false)
}
private fun attemptLogin() { private fun attemptLogin() {
// Reset errors. // Reset errors.
@ -198,45 +220,28 @@ class LoginActivity : AppCompatActivity() {
editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert) editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert)
editor.apply() editor.apply()
val apiDetailsService = AndroidApiDetailsService(this@LoginActivity)
val api = SelfossApi( val api = SelfossApi(
this, // this,
this@LoginActivity, // this@LoginActivity,
isWithSelfSignedCert, // isWithSelfSignedCert,
-1L // -1L
apiDetailsService
) )
if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) { if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) {
api.login().enqueue(object : Callback<SuccessResponse> { CoroutineScope(Dispatchers.IO).launch {
private fun preferenceError(t: Throwable) { try {
editor.remove("url") val result = api.login()
editor.remove("login") if (result != null && result.isSuccess) {
editor.remove("httpUserName")
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)
showProgress(false)
}
override fun onResponse(
call: Call<SuccessResponse>,
response: Response<SuccessResponse>
) {
if (response.body() != null && response.body()!!.isSuccess) {
goToMain() goToMain()
} else { } else {
preferenceError(Exception("No response body...")) preferenceError(Exception("Not success"))
} }
} catch (cause: Throwable) {
Log.e("1", "LOL")
} }
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
preferenceError(t)
}
})
} else { } else {
showProgress(false) showProgress(false)
} }

View File

@ -5,29 +5,33 @@ import android.content.SharedPreferences
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import androidx.preference.PreferenceManager
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import androidx.room.Room import androidx.room.Room
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityReaderBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityReaderBinding
import bou.amine.apps.readerforselfossv2.android.fragments.ArticleFragment import bou.amine.apps.readerforselfossv2.android.fragments.ArticleFragment
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.SharedItems
import bou.amine.apps.readerforselfossv2.android.utils.toggleStar import bou.amine.apps.readerforselfossv2.android.utils.toggleStar
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ReaderActivity : AppCompatActivity() { class ReaderActivity : AppCompatActivity() {
@ -98,10 +102,11 @@ class ReaderActivity : AppCompatActivity() {
activeAlignment = prefs.getInt("text_align", JUSTIFY) activeAlignment = prefs.getInt("text_align", JUSTIFY)
api = SelfossApi( api = SelfossApi(
this, // this,
this@ReaderActivity, // this@ReaderActivity,
settings.getBoolean("isSelfSignedCert", false), // settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong() // prefs.getString("api_timeout", "-1")!!.toLong()
AndroidApiDetailsService(this@ReaderActivity)
) )
if (allItems.isEmpty()) { if (allItems.isEmpty()) {
@ -122,9 +127,11 @@ class ReaderActivity : AppCompatActivity() {
binding.indicator.setViewPager(binding.pager) binding.indicator.setViewPager(binding.pager)
} }
private fun readItem(item: Item) { private fun readItem(item: SelfossModel.Item) {
if (markOnScroll) { if (markOnScroll) {
SharedItems.readItem(applicationContext, api, db, item) CoroutineScope(Dispatchers.IO).launch {
// Todo: SharedItems.readItem(applicationContext, api, db, item)
}
} }
} }
@ -170,7 +177,7 @@ class ReaderActivity : AppCompatActivity() {
inflater.inflate(R.menu.reader_menu, menu) inflater.inflate(R.menu.reader_menu, menu)
toolbarMenu = menu toolbarMenu = menu
if (allItems.isNotEmpty() && allItems[currentItem].starred) { if (allItems.isNotEmpty() && allItems[currentItem].starred == 1) {
canRemoveFromFavorite() canRemoveFromFavorite()
} else { } else {
canFavorite() canFavorite()
@ -187,7 +194,7 @@ class ReaderActivity : AppCompatActivity() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
super.onPageSelected(position) super.onPageSelected(position)
if (allItems[position].starred) { if (allItems[position].starred == 1) {
canRemoveFromFavorite() canRemoveFromFavorite()
} else { } else {
canFavorite() canFavorite()
@ -218,21 +225,27 @@ class ReaderActivity : AppCompatActivity() {
return true return true
} }
R.id.star -> { R.id.star -> {
if (allItems[binding.pager.currentItem].starred) { if (allItems[binding.pager.currentItem].starred == 1) {
SharedItems.unstarItem( CoroutineScope(Dispatchers.IO).launch {
this@ReaderActivity, // Todo:
api, // SharedItems.unstarItem(
db, // this@ReaderActivity,
allItems[binding.pager.currentItem] // api,
) // db,
// allItems[binding.pager.currentItem]
// )
}
afterUnsave() afterUnsave()
} else { } else {
SharedItems.starItem( CoroutineScope(Dispatchers.IO).launch {
this@ReaderActivity, // Todo:
api, // SharedItems.starItem(
db, // this@ReaderActivity,
allItems[binding.pager.currentItem] // api,
) // db,
// allItems[binding.pager.currentItem]
// )
}
afterSave() afterSave()
} }
} }
@ -260,6 +273,6 @@ class ReaderActivity : AppCompatActivity() {
} }
companion object { companion object {
var allItems: ArrayList<Item> = ArrayList() var allItems: ArrayList<SelfossModel.Item> = ArrayList()
} }
} }

View File

@ -9,17 +9,23 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.Toast import android.widget.Toast
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Source
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.util.ArrayList
class SourcesActivity : AppCompatActivity() { class SourcesActivity : AppCompatActivity() {
@ -60,27 +66,27 @@ class SourcesActivity : AppCompatActivity() {
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val apiDetailsService = AndroidApiDetailsService(this@SourcesActivity)
val api = SelfossApi( val api = SelfossApi(
this, // this,
this@SourcesActivity, // this@SourcesActivity,
settings.getBoolean("isSelfSignedCert", false), // settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong() // prefs.getString("api_timeout", "-1")!!.toLong()
apiDetailsService
) )
var items: ArrayList<Source> = ArrayList() var items: ArrayList<SelfossModel.Source>
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = mLayoutManager binding.recyclerView.layoutManager = mLayoutManager
if (this@SourcesActivity.isNetworkAccessible(binding.recyclerView)) { if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) {
api.sources.enqueue(object : Callback<List<Source>> { CoroutineScope(Dispatchers.IO).launch {
override fun onResponse( val response = api.sources()
call: Call<List<Source>>, if (response != null) {
response: Response<List<Source>> items = response
) { val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api,
if (response.body() != null && response.body()!!.isNotEmpty()) { apiDetailsService
items = response.body() as ArrayList<Source> )
}
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
binding.recyclerView.adapter = mAdapter binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged() mAdapter.notifyDataSetChanged()
if (items.isEmpty()) { if (items.isEmpty()) {
@ -90,16 +96,14 @@ class SourcesActivity : AppCompatActivity() {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
} } else {
override fun onFailure(call: Call<List<Source>>, t: Throwable) {
Toast.makeText( Toast.makeText(
this@SourcesActivity, this@SourcesActivity,
R.string.cant_get_sources, R.string.cant_get_sources,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) }
} }
binding.fab.setOnClickListener { binding.fab.setOnClickListener {

View File

@ -2,38 +2,38 @@ package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView.ScaleType import android.widget.ImageView.ScaleType
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.CardItemBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.SharedItems
import bou.amine.apps.readerforselfossv2.android.utils.buildCustomTabsIntent
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.android.utils.shareLink import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.android.utils.sourceAndDateText import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString import bou.amine.apps.readerforselfossv2.utils.DateUtils
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<Item>, override var items: ArrayList<SelfossModel.Item>,
override val api: SelfossApi, override val api: SelfossApi,
override val apiDetailsService: ApiDetailsService,
override val db: AppDatabase, override val db: AppDatabase,
private val helper: CustomTabActivityHelper, private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean, private val internalBrowser: Boolean,
@ -42,7 +42,8 @@ class ItemCardAdapter(
override val appColors: AppColors, override val appColors: AppColors,
override val userIdentifier: String, override val userIdentifier: String,
override val config: Config, override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit override val searchService: SearchService,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
@ -58,30 +59,30 @@ class ItemCardAdapter(
with(holder) { with(holder) {
val itm = items[position] val itm = items[position]
binding.favButton.isSelected = itm.starred binding.favButton.isSelected = itm.starred == 1
binding.title.text = itm.getTitleDecoded() binding.title.text = itm.getTitleDecoded()
binding.title.setOnTouchListener(LinkOnTouchListener()) binding.title.setOnTouchListener(LinkOnTouchListener())
binding.title.setLinkTextColor(appColors.colorAccent) binding.title.setLinkTextColor(appColors.colorAccent)
binding.sourceTitleAndDate.text = itm.sourceAndDateText() binding.sourceTitleAndDate.text = itm.sourceAndDateText(DateUtils(apiDetailsService))
if (!fullHeightCards) { if (!fullHeightCards) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
binding.itemImage.scaleType = ScaleType.CENTER_CROP binding.itemImage.scaleType = ScaleType.CENTER_CROP
} }
if (itm.getThumbnail(c).isEmpty()) { if (itm.getThumbnail(apiDetailsService.getBaseUrl()).isEmpty()) {
binding.itemImage.visibility = View.GONE binding.itemImage.visibility = View.GONE
Glide.with(c).clear(binding.itemImage) Glide.with(c).clear(binding.itemImage)
binding.itemImage.setImageDrawable(null) binding.itemImage.setImageDrawable(null)
} else { } else {
binding.itemImage.visibility = View.VISIBLE binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage) c.bitmapCenterCrop(config, itm.getThumbnail(apiDetailsService.getBaseUrl()), binding.itemImage)
} }
if (itm.getIcon(c).isEmpty()) { if (itm.getIcon(apiDetailsService.getBaseUrl()).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle()) val color = generator.getColor(itm.getSourceTitle())
val drawable = val drawable =
@ -91,7 +92,7 @@ class ItemCardAdapter(
.build(itm.getSourceTitle().toTextDrawableString(c), color) .build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.sourceImage.setImageDrawable(drawable) binding.sourceImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.sourceImage) c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.sourceImage)
} }
} }
} }
@ -110,14 +111,18 @@ class ItemCardAdapter(
binding.favButton.setOnClickListener { binding.favButton.setOnClickListener {
val item = items[bindingAdapterPosition] val item = items[bindingAdapterPosition]
if (isNetworkAvailable(c)) { if (c.isNetworkAvailable()) {
if (item.starred) { if (item.starred == 1) {
SharedItems.unstarItem(c, api, db, item) CoroutineScope(Dispatchers.IO).launch {
item.starred = false // Todo: SharedItems.unstarItem(c, api, db, item)
}
item.starred = 0
binding.favButton.isSelected = false binding.favButton.isSelected = false
} else { } else {
SharedItems.starItem(c, api, db, item) CoroutineScope(Dispatchers.IO).launch {
item.starred = true // Todo: SharedItems.starItem(c, api, db, item)
}
item.starred = 1
binding.favButton.isSelected = true binding.favButton.isSelected = true
} }
} }
@ -145,7 +150,8 @@ class ItemCardAdapter(
customTabsIntent, customTabsIntent,
internalBrowser, internalBrowser,
articleViewer, articleViewer,
app app,
searchService
) )
} }
} }

View File

@ -2,31 +2,30 @@ package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.ListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
import bou.amine.apps.readerforselfossv2.android.utils.buildCustomTabsIntent
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.android.utils.sourceAndDateText import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import kotlin.collections.ArrayList
class ItemListAdapter( class ItemListAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<Item>, override var items: ArrayList<SelfossModel.Item>,
override val api: SelfossApi, override val api: SelfossApi,
override val apiDetailsService: ApiDetailsService,
override val db: AppDatabase, override val db: AppDatabase,
private val helper: CustomTabActivityHelper, private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean, private val internalBrowser: Boolean,
@ -34,7 +33,8 @@ class ItemListAdapter(
override val userIdentifier: String, override val userIdentifier: String,
override val appColors: AppColors, override val appColors: AppColors,
override val config: Config, override val config: Config,
override val updateItems: (ArrayList<Item>) -> Unit override val searchService: SearchService,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() { ) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val c: Context = app.baseContext private val c: Context = app.baseContext
@ -54,11 +54,11 @@ class ItemListAdapter(
binding.title.setLinkTextColor(appColors.colorAccent) binding.title.setLinkTextColor(appColors.colorAccent)
binding.sourceTitleAndDate.text = itm.sourceAndDateText() binding.sourceTitleAndDate.text = itm.sourceAndDateText(DateUtils(apiDetailsService))
if (itm.getThumbnail(c).isEmpty()) { if (itm.getThumbnail(apiDetailsService.getBaseUrl()).isEmpty()) {
if (itm.getIcon(c).isEmpty()) { if (itm.getIcon(apiDetailsService.getBaseUrl()).isEmpty()) {
val color = generator.getColor(itm.getSourceTitle()) val color = generator.getColor(itm.getSourceTitle())
val drawable = val drawable =
@ -69,10 +69,10 @@ class ItemListAdapter(
binding.itemImage.setImageDrawable(drawable) binding.itemImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage) c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage)
} }
} else { } else {
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage) c.bitmapCenterCrop(config, itm.getThumbnail(apiDetailsService.getBaseUrl()), binding.itemImage)
} }
} }
} }
@ -97,7 +97,8 @@ class ItemListAdapter(
customTabsIntent, customTabsIntent,
internalBrowser, internalBrowser,
articleViewer, articleViewer,
app app,
searchService
) )
} }
} }

View File

@ -5,31 +5,37 @@ import android.graphics.Color
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.SharedItems import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.service.SearchService
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() {
abstract var items: ArrayList<Item> abstract var items: ArrayList<SelfossModel.Item>
abstract val api: SelfossApi abstract val api: SelfossApi
abstract val apiDetailsService: ApiDetailsService
abstract val db: AppDatabase abstract val db: AppDatabase
abstract val userIdentifier: String abstract val userIdentifier: String
abstract val app: Activity abstract val app: Activity
abstract val appColors: AppColors abstract val appColors: AppColors
abstract val config: Config abstract val config: Config
abstract val updateItems: (ArrayList<Item>) -> Unit abstract val searchService: SearchService
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
fun updateAllItems() { fun updateAllItems() {
items = SharedItems.focusedItems items = ArrayList() // TODO: SharedItems.focusedItems
notifyDataSetChanged() notifyDataSetChanged()
updateItems(items) updateItems(items)
} }
private fun unmarkSnackbar(i: Item, position: Int) { private fun unmarkSnackbar(i: SelfossModel.Item, position: Int) {
val s = Snackbar val s = Snackbar
.make( .make(
app.findViewById(R.id.coordLayout), app.findViewById(R.id.coordLayout),
@ -37,12 +43,15 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
SharedItems.unreadItem(app, api, db, i) CoroutineScope(Dispatchers.IO).launch {
if (SharedItems.displayedItems == "unread") { // Todo: SharedItems.unreadItem(app, api, db, i)
addItemAtIndex(i, position)
} else {
notifyItemChanged(position)
} }
// Todo:
// if (SharedItems.displayedItems == "unread") {
// addItemAtIndex(i, position)
// } else {
// notifyItemChanged(position)
// }
} }
val view = s.view val view = s.view
@ -59,14 +68,17 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
Snackbar.LENGTH_LONG Snackbar.LENGTH_LONG
) )
.setAction(R.string.undo_string) { .setAction(R.string.undo_string) {
SharedItems.readItem(app, api, db, items[position]) CoroutineScope(Dispatchers.IO).launch {
items = SharedItems.focusedItems // Todo: SharedItems.readItem(app, api, db, items[position])
if (SharedItems.displayedItems == "unread") {
notifyItemRemoved(position)
updateItems(items)
} else {
notifyItemChanged(position)
} }
// Todo: items = SharedItems.focusedItems
// Todo:
// if (SharedItems.displayedItems == "unread") {
// notifyItemRemoved(position)
// updateItems(items)
// } else {
// notifyItemChanged(position)
// }
} }
val view = s.view val view = s.view
@ -76,40 +88,46 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
} }
fun handleItemAtIndex(position: Int) { fun handleItemAtIndex(position: Int) {
if (SharedItems.unreadItemStatusAtIndex(position)) { // Todo:
readItemAtIndex(position) // if (SharedItems.unreadItemStatusAtIndex(position)) {
} else { // readItemAtIndex(position)
unreadItemAtIndex(position) // } else {
} // unreadItemAtIndex(position)
// }
} }
private fun readItemAtIndex(position: Int) { private fun readItemAtIndex(position: Int) {
val i = items[position] val i = items[position]
SharedItems.readItem(app, api, db, i) CoroutineScope(Dispatchers.IO).launch {
if (SharedItems.displayedItems == "unread") { // Todo: SharedItems.readItem(app, api, db, i)
items.remove(i)
notifyItemRemoved(position)
updateItems(items)
} else {
notifyItemChanged(position)
} }
// Todo:
// if (SharedItems.displayedItems == "unread") {
// items.remove(i)
// notifyItemRemoved(position)
// updateItems(items)
// } else {
// notifyItemChanged(position)
// }
unmarkSnackbar(i, position) unmarkSnackbar(i, position)
} }
private fun unreadItemAtIndex(position: Int) { private fun unreadItemAtIndex(position: Int) {
SharedItems.unreadItem(app, api, db, items[position]) CoroutineScope(Dispatchers.IO).launch {
// Todo: SharedItems.unreadItem(app, api, db, items[position])
}
notifyItemChanged(position) notifyItemChanged(position)
markSnackbar(position) markSnackbar(position)
} }
fun addItemAtIndex(item: Item, position: Int) { fun addItemAtIndex(item: SelfossModel.Item, position: Int) {
items.add(position, item) items.add(position, item)
notifyItemInserted(position) notifyItemInserted(position)
updateItems(items) updateItems(items)
} }
fun addItemsAtEnd(newItems: List<Item>) { fun addItemsAtEnd(newItems: List<SelfossModel.Item>) {
val oldSize = items.size val oldSize = items.size
items.addAll(newItems) items.addAll(newItems)
notifyItemRangeInserted(oldSize, newItems.size) notifyItemRangeInserted(oldSize, newItems.size)

View File

@ -2,31 +2,34 @@ package bou.amine.apps.readerforselfossv2.android.adapters
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.Toast import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Source
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SuccessResponse
import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding import bou.amine.apps.readerforselfossv2.android.databinding.SourceListItemBinding
import bou.amine.apps.readerforselfossv2.android.model.getIcon
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import retrofit2.Call import kotlinx.coroutines.CoroutineScope
import retrofit2.Callback import kotlinx.coroutines.Dispatchers
import retrofit2.Response import kotlinx.coroutines.launch
class SourcesListAdapter( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<Source>, private val items: ArrayList<SelfossModel.Source>,
private val api: SelfossApi private val api: SelfossApi,
private val apiDetailsService: ApiDetailsService
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() { ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
@ -42,7 +45,7 @@ class SourcesListAdapter(
val itm = items[position] val itm = items[position]
config = Config(c) config = Config(c)
if (itm.getIcon(c).isEmpty()) { if (itm.getIcon(apiDetailsService.getBaseUrl()).isEmpty()) {
val color = generator.getColor(itm.getTitleDecoded()) val color = generator.getColor(itm.getTitleDecoded())
val drawable = val drawable =
@ -52,7 +55,7 @@ class SourcesListAdapter(
.build(itm.getTitleDecoded().toTextDrawableString(c), color) .build(itm.getTitleDecoded().toTextDrawableString(c), color)
binding.itemImage.setImageDrawable(drawable) binding.itemImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage) c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage)
} }
binding.sourceTitle.text = itm.getTitleDecoded() binding.sourceTitle.text = itm.getTitleDecoded()
@ -71,34 +74,22 @@ class SourcesListAdapter(
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn) val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
deleteBtn.setOnClickListener { deleteBtn.setOnClickListener {
if (c.isNetworkAccessible(null)) { if (c.isNetworkAvailable(null)) {
val (id) = items[adapterPosition] val (id) = items[adapterPosition]
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> { CoroutineScope(Dispatchers.IO).launch {
override fun onResponse( val action = api.deleteSource(id)
call: Call<SuccessResponse>, if (action != null && action.isSuccess) {
response: Response<SuccessResponse> items.removeAt(adapterPosition)
) { notifyItemRemoved(adapterPosition)
if (response.body() != null && response.body()!!.isSuccess) { notifyItemRangeChanged(adapterPosition, itemCount)
items.removeAt(adapterPosition) } else {
notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount)
} else {
Toast.makeText(
app,
R.string.can_delete_source,
Toast.LENGTH_SHORT
).show()
}
}
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
Toast.makeText( Toast.makeText(
app, app,
R.string.can_delete_source, R.string.can_delete_source,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
}) }
} }
} }
} }

View File

@ -1,22 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.selfoss
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
internal class BooleanTypeAdapter : JsonDeserializer<Boolean> {
@Throws(JsonParseException::class)
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): Boolean? =
try {
json.asInt == 1
} catch (e: Exception) {
json.asBoolean
}
}

View File

@ -1,245 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.selfoss
import android.app.Activity
import android.content.Context
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.SharedItems
import bou.amine.apps.readerforselfossv2.android.utils.getUnsafeHttpClient
import com.burgstaller.okhttp.AuthenticationCacheInterceptor
import com.burgstaller.okhttp.CachingAuthenticatorDecorator
import com.burgstaller.okhttp.DispatchingAuthenticator
import com.burgstaller.okhttp.basic.BasicAuthenticator
import com.burgstaller.okhttp.digest.CachingAuthenticator
import com.burgstaller.okhttp.digest.Credentials
import com.burgstaller.okhttp.digest.DigestAuthenticator
import com.google.gson.GsonBuilder
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.net.SocketTimeoutException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
class SelfossApi(
c: Context,
callingActivity: Activity?,
isWithSelfSignedCert: Boolean,
timeout: Long
) {
private lateinit var service: SelfossService
private val config: Config = Config(c)
private val userName: String
private val password: String
fun OkHttpClient.Builder.maybeWithSelfSigned(isWithSelfSignedCert: Boolean): OkHttpClient.Builder =
if (isWithSelfSignedCert) {
getUnsafeHttpClient()
} else {
this
}
fun OkHttpClient.Builder.maybeWithSettingsTimeout(timeout: Long): OkHttpClient.Builder =
if (timeout != -1L) {
this.readTimeout(timeout, TimeUnit.SECONDS)
.connectTimeout(timeout, TimeUnit.SECONDS)
} else {
this
}
fun Credentials.createAuthenticator(): DispatchingAuthenticator =
DispatchingAuthenticator.Builder()
.with("digest", DigestAuthenticator(this))
.with("basic", BasicAuthenticator(this))
.build()
fun DispatchingAuthenticator.getHttpClien(isWithSelfSignedCert: Boolean, timeout: Long): OkHttpClient.Builder {
val authCache = ConcurrentHashMap<String, CachingAuthenticator>()
return OkHttpClient
.Builder()
.maybeWithSettingsTimeout(timeout)
.maybeWithSelfSigned(isWithSelfSignedCert)
.authenticator(CachingAuthenticatorDecorator(this, authCache))
.addInterceptor(AuthenticationCacheInterceptor(authCache))
.addInterceptor(object: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response: Response = chain.proceed(request)
if (response.code == 408) {
return response
}
return response
}
})
}
init {
userName = config.userLogin
password = config.userPassword
val authenticator =
Credentials(
config.httpUserLogin,
config.httpUserPassword
).createAuthenticator()
val gson =
GsonBuilder()
.registerTypeAdapter(Boolean::class.javaPrimitiveType, BooleanTypeAdapter())
.registerTypeAdapter(SelfossTagType::class.java, SelfossTagTypeTypeAdapter())
.setLenient()
.create()
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.NONE
val httpClient = authenticator.getHttpClien(isWithSelfSignedCert, timeout)
val timeoutCode = 504
httpClient
.addInterceptor { chain ->
val res = chain.proceed(chain.request())
if (res.code == timeoutCode) {
throw SocketTimeoutException("timeout")
}
res
}
.addInterceptor(logging)
.addInterceptor { chain ->
val request = chain.request()
try {
chain.proceed(request)
} catch (e: SocketTimeoutException) {
Response.Builder()
.code(timeoutCode)
.protocol(Protocol.HTTP_2)
.body("".toResponseBody("text/plain".toMediaTypeOrNull()))
.message("")
.request(request)
.build()
}
}
try {
val retrofit =
Retrofit
.Builder()
.baseUrl(config.baseUrl)
.client(httpClient.build())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
service = retrofit.create(SelfossService::class.java)
} catch (e: IllegalArgumentException) {
if (callingActivity != null) {
Config.logoutAndRedirect(c, callingActivity, config.settings.edit(), baseUrlFail = true)
}
}
}
fun login(): Call<SuccessResponse> =
service.loginToSelfoss(config.userLogin, config.userPassword)
suspend fun readItems(
itemsNumber: Int,
offset: Int
): retrofit2.Response<List<Item>> =
getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
suspend fun newItems(
itemsNumber: Int,
offset: Int
): retrofit2.Response<List<Item>> =
getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset)
suspend fun starredItems(
itemsNumber: Int,
offset: Int
): retrofit2.Response<List<Item>> =
getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, 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(
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)
fun markItem(itemId: String): Call<SuccessResponse> =
service.markAsRead(itemId, userName, password)
fun unmarkItem(itemId: String): Call<SuccessResponse> =
service.unmarkAsRead(itemId, userName, password)
suspend fun readAll(ids: List<String>): SuccessResponse =
service.markAllAsRead(ids, userName, password)
fun starrItem(itemId: String): Call<SuccessResponse> =
service.starr(itemId, userName, password)
fun unstarrItem(itemId: String): Call<SuccessResponse> =
service.unstarr(itemId, userName, password)
suspend fun stats(): retrofit2.Response<Stats> = service.stats(userName, password)
val tags: Call<List<Tag>>
get() = service.tags(userName, password)
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)
fun deleteSource(id: String): Call<SuccessResponse> =
service.deleteSource(id, userName, password)
fun spouts(): Call<Map<String, Spout>> =
service.spouts(userName, password)
fun createSource(
title: String,
url: String,
spout: String,
tags: String,
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 bou.amine.apps.readerforselfossv2.android.api.selfoss
import android.content.Context
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.utils.SharedItems
import bou.amine.apps.readerforselfossv2.android.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, itemsNumber: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
val response = when (SharedItems.displayedItems) {
"read" -> api.readItems(itemsNumber, 0)
"unread" -> api.newItems(itemsNumber, 0)
"starred" -> api.starredItems(itemsNumber, 0)
else -> api.readItems(itemsNumber, 0)
}
if (response.isSuccessful) {
SharedItems.refreshFocusedItems(response.body() as ArrayList<Item>)
SharedItems.updateDatabase(db)
}
}
}
suspend fun getReadItems(context: Context, api: SelfossApi, db: AppDatabase, itemsNumber: Int, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
enqueueArticles(api.readItems( itemsNumber, offset), db, false)
SharedItems.fetchedAll = true
SharedItems.updateDatabase(db)
} catch (e: Throwable) {}
}
}
suspend fun getUnreadItems(context: Context, api: SelfossApi, db: AppDatabase, itemsNumber: Int, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
if (!SharedItems.fetchedUnread) {
SharedItems.clearDBItems(db)
}
enqueueArticles(api.newItems(itemsNumber, offset), db, false)
SharedItems.fetchedUnread = true
} catch (e: Throwable) {}
}
SharedItems.updateDatabase(db)
}
suspend fun getStarredItems(context: Context, api: SelfossApi, db: AppDatabase, itemsNumber: Int, offset: Int) = withContext(Dispatchers.IO) {
if (isNetworkAvailable(context)) {
try {
enqueueArticles(api.starredItems(itemsNumber, 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

@ -1,253 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.selfoss
import android.content.Context
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import android.text.Html
import android.webkit.URLUtil
import org.jsoup.Jsoup
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.google.gson.annotations.SerializedName
import java.util.*
import kotlin.collections.ArrayList
private fun constructUrl(config: Config?, path: String, file: String?): String {
return if (file.isEmptyOrNullOrNullString()) {
""
} else {
val baseUriBuilder = Uri.parse(config!!.baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
}
}
data class Tag(
@SerializedName("tag") val tag: String,
@SerializedName("color") val color: String,
@SerializedName("unread") val unread: Int
) {
fun getTitleDecoded(): String {
return Html.fromHtml(tag).toString()
}
}
class SuccessResponse(@SerializedName("success") val success: Boolean) {
val isSuccess: Boolean
get() = success
}
class Stats(
@SerializedName("total") val total: Int,
@SerializedName("unread") val unread: Int,
@SerializedName("starred") val starred: Int
)
data class Spout(
@SerializedName("name") val name: String,
@SerializedName("description") val description: String
)
data class ApiVersion(
@SerializedName("version") val version: String?,
@SerializedName("apiversion") val apiversion: String?
) {
fun getApiMajorVersion() : Int {
var versionNumber = 0
if (apiversion != null) {
versionNumber = apiversion.substringBefore(".").toInt()
}
return versionNumber
}
}
data class Source(
@SerializedName("id") val id: String,
@SerializedName("title") val title: String,
@SerializedName("tags") val tags: SelfossTagType,
@SerializedName("spout") val spout: String,
@SerializedName("error") val error: String,
@SerializedName("icon") val icon: String
) {
var config: Config? = null
fun getIcon(app: Context): String {
if (config == null) {
config = Config(app)
}
return constructUrl(config, "favicons", icon)
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
}
data class Item(
@SerializedName("id") val id: String,
@SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String,
@SerializedName("content") val content: String,
@SerializedName("unread") var unread: Boolean,
@SerializedName("starred") var starred: Boolean,
@SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?,
@SerializedName("link") val link: String,
@SerializedName("sourcetitle") val sourcetitle: String,
@SerializedName("tags") val tags: SelfossTagType
) : Parcelable {
var config: Config? = null
companion object {
@JvmField val CREATOR: Parcelable.Creator<Item> = object : Parcelable.Creator<Item> {
override fun createFromParcel(source: Parcel): Item = Item(source)
override fun newArray(size: Int): Array<Item?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
id = source.readString().orEmpty(),
datetime = source.readString().orEmpty(),
title = source.readString().orEmpty(),
content = source.readString().orEmpty(),
unread = 0.toByte() != source.readByte(),
starred = 0.toByte() != source.readByte(),
thumbnail = source.readString(),
icon = source.readString(),
link = source.readString().orEmpty(),
sourcetitle = source.readString().orEmpty(),
tags = if (source.readParcelable<SelfossTagType>(ClassLoader.getSystemClassLoader()) != null) source.readParcelable(ClassLoader.getSystemClassLoader())!! else SelfossTagType("")
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(id)
dest.writeString(datetime)
dest.writeString(title)
dest.writeString(content)
dest.writeByte((if (unread) 1 else 0))
dest.writeByte((if (starred) 1 else 0))
dest.writeString(thumbnail)
dest.writeString(icon)
dest.writeString(link)
dest.writeString(sourcetitle)
dest.writeParcelable(tags, flags)
}
fun getIcon(app: Context): String {
if (config == null) {
config = Config(app)
}
return constructUrl(config, "favicons", icon)
}
fun getThumbnail(app: Context): String {
if (config == null) {
config = Config(app)
}
return constructUrl(config, "thumbnails", thumbnail)
}
fun getImages() : ArrayList<String> {
val allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
fun preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try {
for (url in imageUrls) {
if ( URLUtil.isValidUrl(url)) {
val image = Glide.with(context).asBitmap()
.apply(glideOptions)
.load(url).submit()
}
}
} catch (e : Error) {
return false
}
return true
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
fun getSourceTitle(): String {
return Html.fromHtml(sourcetitle).toString()
}
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
}
data class SelfossTagType(val tags: String) : Parcelable {
companion object {
@JvmField val CREATOR: Parcelable.Creator<SelfossTagType> =
object : Parcelable.Creator<SelfossTagType> {
override fun createFromParcel(source: Parcel): SelfossTagType =
SelfossTagType(source)
override fun newArray(size: Int): Array<SelfossTagType?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
tags = source.readString().orEmpty()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(tags)
}
}

View File

@ -1,141 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.selfoss
import retrofit2.Call
import retrofit2.Response
import retrofit2.http.DELETE
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
internal interface SelfossService {
@GET("login")
fun loginToSelfoss(@Query("username") username: String, @Query("password") password: String): Call<SuccessResponse>
@GET("items")
suspend 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>>
@GET("items")
fun allItems(
@Query("username") username: String,
@Query("password") password: String
): Call<List<Item>>
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("mark/{id}")
fun markAsRead(
@Path("id") id: String,
@Query("username") username: String,
@Query("password") password: String
): Call<SuccessResponse>
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("unmark/{id}")
fun unmarkAsRead(
@Path("id") id: String,
@Query("username") username: String,
@Query("password") password: String
): Call<SuccessResponse>
@FormUrlEncoded
@POST("mark")
suspend fun markAllAsRead(
@Field("ids[]") ids: List<String>,
@Query("username") username: String,
@Query("password") password: String
): SuccessResponse
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("starr/{id}")
fun starr(
@Path("id") id: String,
@Query("username") username: String,
@Query("password") password: String
): Call<SuccessResponse>
@Headers("Content-Type: application/x-www-form-urlencoded")
@POST("unstarr/{id}")
fun unstarr(
@Path("id") id: String,
@Query("username") username: String,
@Query("password") password: String
): Call<SuccessResponse>
@GET("stats")
suspend fun stats(
@Query("username") username: String,
@Query("password") password: String
): Response<Stats>
@GET("tags")
fun tags(
@Query("username") username: String,
@Query("password") password: String
): Call<List<Tag>>
@GET("update")
fun update(
@Query("username") username: String,
@Query("password") password: String
): Call<String>
@GET("sources/spouts")
fun spouts(
@Query("username") username: String,
@Query("password") password: String
): Call<Map<String, Spout>>
@GET("sources/list")
fun sources(
@Query("username") username: String,
@Query("password") password: String
): Call<List<Source>>
@GET("api/about")
fun version(): Call<ApiVersion>
@DELETE("source/{id}")
fun deleteSource(
@Path("id") id: String,
@Query("username") username: String,
@Query("password") password: String
): Call<SuccessResponse>
@FormUrlEncoded
@POST("source")
fun createSource(
@Field("title") title: String,
@Field("url") url: String,
@Field("spout") spout: String,
@Field("tags") tags: String,
@Field("filter") filter: String,
@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

@ -1,22 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.api.selfoss
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import java.lang.reflect.Type
internal class SelfossTagTypeTypeAdapter : JsonDeserializer<SelfossTagType> {
@Throws(JsonParseException::class)
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): SelfossTagType? =
if (json.isJsonArray) {
SelfossTagType(json.asJsonArray.joinToString(",") { it.toString() })
} else {
SelfossTagType(json.toString())
}
}

View File

@ -14,22 +14,27 @@ import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import bou.amine.apps.readerforselfossv2.android.MainActivity import bou.amine.apps.readerforselfossv2.android.MainActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi import bou.amine.apps.readerforselfossv2.android.model.preloadImages
import bou.amine.apps.readerforselfossv2.android.api.selfoss.getAndStoreAllItems import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabaseService
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.SharedItems
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.service.SelfossService
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.concurrent.thread import kotlin.concurrent.thread
@ -43,14 +48,20 @@ override fun doWork(): Result {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context) val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false) val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
if (periodicRefresh) { if (periodicRefresh) {
val apiDetailsService = AndroidApiDetailsService(this.context)
val api = SelfossApi( val api = SelfossApi(
this.context, // this.context,
null, // null,
settings.getBoolean("isSelfSignedCert", false), // settings.getBoolean("isSelfSignedCert", false),
sharedPref.getString("api_timeout", "-1")!!.toLong() // sharedPref.getString("api_timeout", "-1")!!.toLong()
apiDetailsService
) )
if (isNetworkAvailable(context)) { val dateUtils = DateUtils(apiDetailsService)
val searchService = SearchService(dateUtils)
val service = SelfossService(api, AndroidDeviceDatabaseService(AndroidDeviceDatabase(applicationContext), searchService), searchService)
if (context.isNetworkAvailable()) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val notificationManager = val notificationManager =
@ -80,26 +91,26 @@ override fun doWork(): Result {
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail( action.read -> doAndReportOnFail(
api.markItem(action.articleId), api.markAsRead(action.articleId),
action action
) )
action.unread -> doAndReportOnFail( action.unread -> doAndReportOnFail(
api.unmarkItem(action.articleId), api.unmarkAsRead(action.articleId),
action action
) )
action.starred -> doAndReportOnFail( action.starred -> doAndReportOnFail(
api.starrItem(action.articleId), api.starr(action.articleId),
action action
) )
action.unstarred -> doAndReportOnFail( action.unstarred -> doAndReportOnFail(
api.unstarrItem(action.articleId), api.unstarr(action.articleId),
action action
) )
} }
} }
getAndStoreAllItems(context, api, db) service.getAndStoreAllItems(context.isNetworkAvailable())
SharedItems.updateDatabase(db) // TODO: SharedItems.updateDatabase(db, dateUtils)
storeItems(notifyNewItems, notificationManager) storeItems(notifyNewItems, notificationManager)
} }
} }
@ -109,10 +120,10 @@ override fun doWork(): Result {
private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) { private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val apiItems = SharedItems.items val apiItems = emptyList<SelfossModel.Item>() // TODO: SharedItems.items
val newSize = apiItems.filter { it.unread }.size val newSize = apiItems.filter { it.unread == 1 }.size
if (notifyNewItems && newSize > 0) { if (notifyNewItems && newSize > 0) {
val intent = Intent(context, MainActivity::class.java).apply { val intent = Intent(context, MainActivity::class.java).apply {
@ -151,19 +162,11 @@ override fun doWork(): Result {
} }
} }
private fun <T> doAndReportOnFail(call: Call<T>, action: ActionEntity) { private fun doAndReportOnFail(result: SelfossModel.SuccessResponse?, action: ActionEntity) {
call.enqueue(object : Callback<T> { if (result != null && result.isSuccess) {
override fun onResponse( thread {
call: Call<T>, db.actionsDao().delete(action)
response: Response<T>
) {
thread {
db.actionsDao().delete(action)
}
} }
}
override fun onFailure(call: Call<T>, t: Throwable) {
}
})
} }
} }

View File

@ -10,39 +10,51 @@ import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceManager
import android.view.* import android.view.*
import android.webkit.* import android.webkit.*
import android.widget.Toast import android.widget.Toast
import com.google.android.material.floatingactionbutton.FloatingActionButton
import androidx.fragment.app.Fragment
import androidx.core.widget.NestedScrollView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import androidx.room.Room import androidx.room.Room
import bou.amine.apps.readerforselfossv2.android.ImageActivity import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi
import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding
import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabaseService
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.* import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAccessible import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.utils.* import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.service.SelfossService
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -50,11 +62,13 @@ import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import kotlin.collections.ArrayList
class ArticleFragment : Fragment() { class ArticleFragment : Fragment() {
private lateinit var dbService: AndroidDeviceDatabaseService
private lateinit var apiDetailsService: ApiDetailsService
private lateinit var service: SelfossService<AndroidItemEntity>
private var fontSize: Int = 16 private var fontSize: Int = 16
private lateinit var item: Item private lateinit var item: SelfossModel.Item
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
private lateinit var url: String private lateinit var url: String
private lateinit var contentText: String private lateinit var contentText: String
@ -91,6 +105,12 @@ class ArticleFragment : Fragment() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
apiDetailsService = AndroidApiDetailsService(requireContext())
dbService = AndroidDeviceDatabaseService(AndroidDeviceDatabase(requireContext()), SearchService(DateUtils(apiDetailsService)))
service = SelfossService(SelfossApi(apiDetailsService), dbService, SearchService(DateUtils(apiDetailsService)))
item = requireArguments().getParcelable(ARG_ITEMS)!! item = requireArguments().getParcelable(ARG_ITEMS)!!
db = Room.databaseBuilder( db = Room.databaseBuilder(
@ -110,8 +130,8 @@ class ArticleFragment : Fragment() {
url = item.getLinkDecoded() url = item.getLinkDecoded()
contentText = item.content contentText = item.content
contentTitle = item.getTitleDecoded() contentTitle = item.getTitleDecoded()
contentImage = item.getThumbnail(requireActivity()) contentImage = item.getThumbnail(apiDetailsService.getBaseUrl())
contentSource = item.sourceAndDateText() contentSource = item.sourceAndDateText(DateUtils(apiDetailsService))
allImages = item.getImages() allImages = item.getImages()
prefs = PreferenceManager.getDefaultSharedPreferences(activity) prefs = PreferenceManager.getDefaultSharedPreferences(activity)
@ -136,10 +156,11 @@ class ArticleFragment : Fragment() {
val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
val api = SelfossApi( val api = SelfossApi(
requireContext(), // requireContext(),
requireActivity(), // requireActivity(),
settings.getBoolean("isSelfSignedCert", false), // settings.getBoolean("isSelfSignedCert", false),
prefs.getString("api_timeout", "-1")!!.toLong() // prefs.getString("api_timeout", "-1")!!.toLong()
apiDetailsService
) )
fab = binding.fab fab = binding.fab
@ -166,27 +187,33 @@ class ArticleFragment : Fragment() {
R.id.share_action -> requireActivity().shareLink(url, contentTitle) R.id.share_action -> requireActivity().shareLink(url, contentTitle)
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item) R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
R.id.unread_action -> if (context != null) { R.id.unread_action -> if (context != null) {
if (this@ArticleFragment.item.unread) { if (this@ArticleFragment.item.unread == 1) {
SharedItems.readItem( CoroutineScope(Dispatchers.IO).launch {
context!!, // TODO:
api, // dbService.readItem(
db, // context!!,
this@ArticleFragment.item // api,
) // db,
this@ArticleFragment.item.unread = false // this@ArticleFragment.item
// )
}
this@ArticleFragment.item.unread = 0
Toast.makeText( Toast.makeText(
context, context,
R.string.marked_as_read, R.string.marked_as_read,
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} else { } else {
SharedItems.unreadItem( CoroutineScope(Dispatchers.IO).launch {
context!!, // TODO
api, // .unreadItem(
db, // context!!,
this@ArticleFragment.item // api,
) // db,
this@ArticleFragment.item.unread = true // this@ArticleFragment.item
// )
}
this@ArticleFragment.item.unread = 1
Toast.makeText( Toast.makeText(
context, context,
R.string.marked_as_unread, R.string.marked_as_unread,
@ -284,7 +311,7 @@ class ArticleFragment : Fragment() {
} }
private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) { private fun getContentFromMercury(customTabsIntent: CustomTabsIntent) {
if ((context != null && requireContext().isNetworkAccessible(null)) || context == null) { if ((context != null && requireContext().isNetworkAvailable(null)) || context == null) {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE
val parser = MercuryApi() val parser = MercuryApi()
@ -535,11 +562,11 @@ class ArticleFragment : Fragment() {
private const val ARG_ITEMS = "items" private const val ARG_ITEMS = "items"
fun newInstance( fun newInstance(
item: Item item: SelfossModel.Item
): ArticleFragment { ): ArticleFragment {
val fragment = ArticleFragment() val fragment = ArticleFragment()
val args = Bundle() val args = Bundle()
args.putParcelable(ARG_ITEMS, item) args.putParcelable(ARG_ITEMS, item.toParcelable())
fragment.arguments = args fragment.arguments = args
return fragment return fragment
} }

View File

@ -0,0 +1,125 @@
package bou.amine.apps.readerforselfossv2.android.model
import android.content.Context
import android.net.Uri
import android.text.Html
import android.webkit.URLUtil
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import org.jsoup.Jsoup
import java.util.*
/**
* Items extension methods
*/
fun SelfossModel.Item.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun SelfossModel.Item.getThumbnail(baseUrl: String): String {
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
fun SelfossModel.Item.getImages() : ArrayList<String> {
val allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(Locale.US).contains(".jpg") ||
url.lowercase(Locale.US).contains(".jpeg") ||
url.lowercase(Locale.US).contains(".png") ||
url.lowercase(Locale.US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
fun SelfossModel.Item.preloadImages(context: Context) : Boolean {
val imageUrls = this.getImages()
val glideOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL).timeout(10000)
try {
for (url in imageUrls) {
if ( URLUtil.isValidUrl(url)) {
Glide.with(context).asBitmap()
.apply(glideOptions)
.load(url).submit()
}
}
} catch (e : Error) {
return false
}
return true
}
fun SelfossModel.Item.getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
fun SelfossModel.Item.getSourceTitle(): String {
return Html.fromHtml(sourcetitle).toString()
}
// TODO: maybe find a better way to handle these kind of urls
fun SelfossModel.Item.getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
/**
* Sources extension methods
*/
fun SelfossModel.Source.getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun SelfossModel.Source.getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
/**
* Common methods
*/
private fun constructUrl(baseUrl: String, path: String, file: String?): String {
return if (file == null || file == "null" || file.isEmpty()) {
""
} else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
}
}

View File

@ -0,0 +1,73 @@
package bou.amine.apps.readerforselfossv2.android.model
import android.os.Parcel
import android.os.Parcelable
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import com.google.gson.annotations.SerializedName
fun SelfossModel.Item.toParcelable() : ParecelableItem =
ParecelableItem(
this.id,
this.datetime,
this.title,
this.content,
this.unread,
this.starred,
this.thumbnail,
this.icon,
this.link,
this.sourcetitle,
this.tags
)
data class ParecelableItem(
@SerializedName("id") val id: String,
@SerializedName("datetime") val datetime: String,
@SerializedName("title") val title: String,
@SerializedName("content") val content: String,
@SerializedName("unread") var unread: Int,
@SerializedName("starred") var starred: Int,
@SerializedName("thumbnail") val thumbnail: String?,
@SerializedName("icon") val icon: String?,
@SerializedName("link") val link: String,
@SerializedName("sourcetitle") val sourcetitle: String,
@SerializedName("tags") val tags: String
) : Parcelable {
companion object {
@JvmField
val CREATOR: Parcelable.Creator<ParecelableItem> = object : Parcelable.Creator<ParecelableItem> {
override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source)
override fun newArray(size: Int): Array<ParecelableItem?> = arrayOfNulls(size)
}
}
constructor(source: Parcel) : this(
id = source.readString().orEmpty(),
datetime = source.readString().orEmpty(),
title = source.readString().orEmpty(),
content = source.readString().orEmpty(),
unread = source.readInt(),
starred = source.readInt(),
thumbnail = source.readString(),
icon = source.readString(),
link = source.readString().orEmpty(),
sourcetitle = source.readString().orEmpty(),
tags = source.readString().orEmpty()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) {
dest.writeString(id)
dest.writeString(datetime)
dest.writeString(title)
dest.writeString(content)
dest.writeInt(unread)
dest.writeInt(starred)
dest.writeString(thumbnail)
dest.writeString(icon)
dest.writeString(link)
dest.writeString(sourcetitle)
dest.writeString(tags)
}
}

View File

@ -0,0 +1,28 @@
package bou.amine.apps.readerforselfossv2.android.persistence
import android.content.Context
import androidx.room.Room
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.dao.DeviceDatabase
class AndroidDeviceDatabase(applicationContext: Context): DeviceDatabase<AndroidItemEntity> {
var db: AppDatabase = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "selfoss-database"
).addMigrations(MIGRATION_1_2).addMigrations(MIGRATION_2_3).addMigrations(MIGRATION_3_4).build()
override suspend fun items(): List<AndroidItemEntity> = db.itemsDao().items()
override suspend fun insertAllItems(vararg items: AndroidItemEntity) = db.itemsDao().insertAllItems(*items)
override suspend fun deleteAllItems() = db.itemsDao().deleteAllItems()
override suspend fun delete(item: AndroidItemEntity) = db.itemsDao().delete(item)
override suspend fun updateItem(item: AndroidItemEntity) = db.itemsDao().updateItem(item)
}

View File

@ -0,0 +1,40 @@
package bou.amine.apps.readerforselfossv2.android.persistence
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.DeviceDataBaseService
import bou.amine.apps.readerforselfossv2.service.SearchService
class AndroidDeviceDatabaseService(db: AndroidDeviceDatabase, searchService: SearchService) :
DeviceDataBaseService<AndroidItemEntity>(db, searchService) {
override suspend fun updateDatabase() {
if (itemsCaching) {
if (items.isEmpty()) {
getFromDB()
}
db.deleteAllItems()
db.insertAllItems(*(items.map { it.toEntity() }).toTypedArray())
}
}
override suspend fun clearDBItems() {
db.deleteAllItems()
}
override fun appendNewItems(newItems: List<SelfossModel.Item>) {
var tmpItems = items
if (tmpItems != newItems) {
tmpItems = tmpItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList<SelfossModel.Item>
tmpItems.addAll(newItems)
items = tmpItems
sortItems()
getFocusedItems()
}
}
override fun getFromDB() {
TODO("Not yet implemented")
}
}

View File

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

View File

@ -6,11 +6,11 @@ import bou.amine.apps.readerforselfossv2.android.persistence.dao.ActionsDao
import bou.amine.apps.readerforselfossv2.android.persistence.dao.DrawerDataDao import bou.amine.apps.readerforselfossv2.android.persistence.dao.DrawerDataDao
import bou.amine.apps.readerforselfossv2.android.persistence.dao.ItemsDao import bou.amine.apps.readerforselfossv2.android.persistence.dao.ItemsDao
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ItemEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
@Database(entities = [TagEntity::class, SourceEntity::class, ItemEntity::class, ActionEntity::class], version = 4) @Database(entities = [TagEntity::class, SourceEntity::class, AndroidItemEntity::class, ActionEntity::class], version = 4)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun drawerDataDao(): DrawerDataDao abstract fun drawerDataDao(): DrawerDataDao

View File

@ -3,9 +3,10 @@ package bou.amine.apps.readerforselfossv2.android.persistence.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
@Entity(tableName = "items") @Entity(tableName = "items")
data class ItemEntity( data class AndroidItemEntity(
@PrimaryKey @PrimaryKey
@ColumnInfo(name = "id") @ColumnInfo(name = "id")
val id: String, val id: String,

View File

@ -0,0 +1,47 @@
package bou.amine.apps.readerforselfossv2.android.service
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
class AndroidApiDetailsService(c: Context) : ApiDetailsService {
val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(c)
private var apiVersion: Int = -1
private var baseUrl: String = ""
private var userName: String = ""
private var password: String = ""
override fun logApiCalls(message: String) {
Log.d("LogApiCalls", message)
}
override fun getApiVersion(): Int {
if (apiVersion != -1) {
apiVersion = settings.getInt("apiVersion", -1)!!
}
return apiVersion
}
override fun getBaseUrl(): String {
if (baseUrl.isEmpty()) {
baseUrl = settings.getString("url", "")!!
}
return baseUrl
}
override fun getUserName(): String {
if (userName.isEmpty()) {
userName = settings.getString("login", "")!!
}
return userName
}
override fun getPassword(): String {
if (password.isEmpty()) {
password = settings.getString("password", "")!!
}
return password
}
}

View File

@ -1,7 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SuccessResponse
import retrofit2.Response
fun Response<SuccessResponse>.succeeded(): Boolean =
this.code() === 200 && this.body() != null && this.body()!!.isSuccess

View File

@ -3,27 +3,7 @@ package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
fun String?.isEmptyOrNullOrNullString(): Boolean =
this == null || this == "null" || this.isEmpty()
fun String.longHash(): Long {
var h = 98764321261L
val l = this.length
val chars = this.toCharArray()
for (i in 0 until l) {
h = 31 * h + chars[i].code.toLong()
}
return h
}
fun String.toStringUriWithHttp(): String =
if (!this.startsWith("https://") && !this.startsWith("http://")) {
"http://" + this
} else {
this
}
fun Context.shareLink(itemUrl: String, itemTitle: String) { fun Context.shareLink(itemUrl: String, itemTitle: String) {
val sendIntent = Intent() val sendIntent = Intent()

View File

@ -1,31 +0,0 @@
package bou.amine.apps.readerforselfossv2.android.utils
import android.text.format.DateUtils
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
fun parseDate(dateString: String): Instant {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (Config.apiVersion >= 4) {
OffsetDateTime.parse(dateString).toInstant()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)
}
}
fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
return " " + DateUtils.getRelativeTimeSpanString(
date.toEpochMilli(),
Instant.now().toEpochMilli(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
)
}

View File

@ -1,8 +1,10 @@
package bou.amine.apps.readerforselfossv2.android.utils package bou.amine.apps.readerforselfossv2.android.utils
import android.content.Context import android.content.Context
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossTagType import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.utils.parseRelativeDate
fun String.toTextDrawableString(c: Context): String { fun String.toTextDrawableString(c: Context): String {
val textDrawable = StringBuilder() val textDrawable = StringBuilder()
@ -15,22 +17,22 @@ fun String.toTextDrawableString(c: Context): String {
return textDrawable.toString() return textDrawable.toString()
} }
fun Item.sourceAndDateText(): String { fun SelfossModel.Item.sourceAndDateText(dateUtils: DateUtils): String {
val formattedDate = parseRelativeDate(this.datetime) val formattedDate = parseRelativeDate(dateUtils)
return this.getSourceTitle() + formattedDate return getSourceTitle() + formattedDate
} }
fun Item.toggleStar(): Item { fun SelfossModel.Item.toggleStar(): SelfossModel.Item {
this.starred = !this.starred this.starred = if (this.starred == 0) 1 else 0
return this return this
} }
fun List<Item>.flattenTags(): List<Item> = fun List<SelfossModel.Item>.flattenTags(): List<SelfossModel.Item> =
this.flatMap { this.flatMap {
val item = it val item = it
val tags: List<String> = it.tags.tags.split(",") val tags: List<String> = it.tags.split(",")
tags.map { t -> tags.map { t ->
item.copy(tags = SelfossTagType(t.trim())) item.copy(tags = t.trim())
} }
} }

View File

@ -18,8 +18,11 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.ReaderActivity import bou.amine.apps.readerforselfossv2.android.ReaderActivity
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item import bou.amine.apps.readerforselfossv2.android.model.getLinkDecoded
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
fun Context.buildCustomTabsIntent(): CustomTabsIntent { fun Context.buildCustomTabsIntent(): CustomTabsIntent {
@ -71,16 +74,17 @@ fun Context.buildCustomTabsIntent(): CustomTabsIntent {
} }
fun Context.openItemUrlInternally( fun Context.openItemUrlInternally(
allItems: ArrayList<Item>, allItems: ArrayList<SelfossModel.Item>,
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
customTabsIntent: CustomTabsIntent, customTabsIntent: CustomTabsIntent,
articleViewer: Boolean, articleViewer: Boolean,
app: Activity app: Activity,
searchService: SearchService
) { ) {
if (articleViewer) { if (articleViewer) {
ReaderActivity.allItems = allItems ReaderActivity.allItems = allItems
SharedItems.position = currentItem searchService.position = currentItem
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)
@ -113,13 +117,14 @@ fun Context.openItemUrlInternalBrowser(
} }
fun Context.openItemUrl( fun Context.openItemUrl(
allItems: ArrayList<Item>, allItems: ArrayList<SelfossModel.Item>,
currentItem: Int, currentItem: Int,
linkDecoded: String, linkDecoded: String,
customTabsIntent: CustomTabsIntent, customTabsIntent: CustomTabsIntent,
internalBrowser: Boolean, internalBrowser: Boolean,
articleViewer: Boolean, articleViewer: Boolean,
app: Activity app: Activity,
searchService: SearchService
) { ) {
if (!linkDecoded.isUrlValid()) { if (!linkDecoded.isUrlValid()) {
@ -138,7 +143,8 @@ fun Context.openItemUrl(
linkDecoded, linkDecoded,
customTabsIntent, customTabsIntent,
articleViewer, articleViewer,
app app,
searchService
) )
} else { } else {
this.openItemUrlInternalBrowser( this.openItemUrlInternalBrowser(
@ -174,7 +180,7 @@ fun String.isBaseUrlValid(ctx: Context): Boolean {
return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash return Patterns.WEB_URL.matcher(this).matches() && existsAndEndsWithSlash
} }
fun Context.openInBrowserAsNewTask(i: Item) { fun Context.openInBrowserAsNewTask(i: SelfossModel.Item) {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp()) intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())

View File

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

View File

@ -14,8 +14,11 @@ var snackBarShown = false
var view: View? = null var view: View? = null
lateinit var s: Snackbar lateinit var s: Snackbar
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean { fun Context.isNetworkAvailable(
val networkIsAccessible = isNetworkAvailable(this) v: View? = null,
overrideOffline: Boolean = false
): Boolean {
val networkIsAccessible = isNetworkAccessible(this)
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) { if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
view = v view = v
@ -43,22 +46,22 @@ fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boo
return if(overrideOffline) overrideOffline else networkIsAccessible return if(overrideOffline) overrideOffline else networkIsAccessible
} }
fun isNetworkAvailable(context: Context): Boolean { private fun isNetworkAccessible(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val network = connectivityManager.activeNetwork ?: return false val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return when { return when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
else -> false else -> false
} }
} else { } else {
val network = connectivityManager.activeNetworkInfo ?: return false val network = connectivityManager.activeNetworkInfo ?: return false
return network.isConnectedOrConnecting return network.isConnectedOrConnecting
} }
} }

View File

@ -1,73 +1,72 @@
package bou.amine.apps.readerforselfossv2.android.utils.persistence package bou.amine.apps.readerforselfossv2.android.utils.persistence
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossTagType import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Source import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Tag
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.SourceEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.TagEntity
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
fun TagEntity.toView(): Tag = fun TagEntity.toView(): SelfossModel.Tag =
Tag( SelfossModel.Tag(
this.tag, this.tag,
this.color, this.color,
this.unread this.unread
) )
fun SourceEntity.toView(): Source = fun SourceEntity.toView(): SelfossModel.Source =
Source( SelfossModel.Source(
this.id, this.id,
this.title, this.title,
SelfossTagType(this.tags), this.tags.split(","),
this.spout, this.spout,
this.error, this.error,
this.icon this.icon
) )
fun Source.toEntity(): SourceEntity = fun SelfossModel.Source.toEntity(): SourceEntity =
SourceEntity( SourceEntity(
this.id, this.id,
this.getTitleDecoded(), this.getTitleDecoded(),
this.tags.tags, this.tags.joinToString(","),
this.spout, this.spout,
this.error, this.error,
this.icon.orEmpty() this.icon
) )
fun Tag.toEntity(): TagEntity = fun SelfossModel.Tag.toEntity(): TagEntity =
TagEntity( TagEntity(
this.tag, this.tag,
this.color, this.color,
this.unread this.unread
) )
fun ItemEntity.toView(): Item = fun AndroidItemEntity.toView(): SelfossModel.Item =
Item( SelfossModel.Item(
this.id, this.id,
this.datetime, this.datetime,
this.title, this.title,
this.content, this.content,
this.unread, if (this.unread) 1 else 0,
this.starred, if (this.starred) 1 else 0,
this.thumbnail, this.thumbnail,
this.icon, this.icon,
this.link, this.link,
this.sourcetitle, this.sourcetitle,
SelfossTagType(this.tags) this.tags
) )
fun Item.toEntity(): ItemEntity = fun SelfossModel.Item.toEntity(): AndroidItemEntity =
ItemEntity( AndroidItemEntity(
this.id, this.id,
this.datetime, this.datetime,
this.getTitleDecoded(), this.getTitleDecoded(),
this.content, this.content,
this.unread, this.unread == 1,
this.starred, this.starred == 1,
this.thumbnail, this.thumbnail,
this.icon, this.icon,
this.link, this.link,
this.getSourceTitle(), this.getSourceTitle(),
this.tags.tags this.tags
) )

View File

@ -6,7 +6,7 @@ buildscript {
} }
dependencies { dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
classpath("com.android.tools.build:gradle:7.1.2") classpath("com.android.tools.build:gradle:7.2.0")
} }
} }

View File

@ -1,6 +1,6 @@
#Wed Feb 09 17:05:19 CET 2022 #Wed Feb 09 17:05:19 CET 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@ -20,9 +20,12 @@ kotlin {
sourceSets { sourceSets {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("io.ktor:ktor-client-core:1.6.7") implementation("io.ktor:ktor-client-core:2.0.1")
implementation("io.ktor:ktor-client-serialization:1.6.7") implementation("io.ktor:ktor-client-content-negotiation:2.0.1")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.0.1")
implementation("io.ktor:ktor-client-logging:2.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("io.ktor:ktor-client-auth:2.0.1")
implementation("org.jsoup:jsoup:1.14.3") implementation("org.jsoup:jsoup:1.14.3")
} }
} }
@ -34,7 +37,7 @@ kotlin {
} }
val androidMain by getting { val androidMain by getting {
dependencies { dependencies {
implementation("io.ktor:ktor-client-android:1.6.7") implementation("io.ktor:ktor-client-android:2.0.1")
} }
} }
val androidTest by getting { val androidTest by getting {
@ -60,7 +63,7 @@ kotlin {
iosX64Test.dependsOn(this) iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this) iosArm64Test.dependsOn(this)
dependencies { dependencies {
implementation("io.ktor:ktor-client-ios:1.6.7") implementation("io.ktor:ktor-client-ios:2.0.1")
} }
//iosSimulatorArm64Test.dependsOn(this) //iosSimulatorArm64Test.dependsOn(this)
} }

View File

@ -0,0 +1,9 @@
package bou.amine.apps.readerforselfossv2.dao
interface DeviceDatabase<ItemEntity> {
suspend fun items(): List<ItemEntity>
suspend fun insertAllItems(vararg items: ItemEntity)
suspend fun deleteAllItems()
suspend fun delete(item: ItemEntity)
suspend fun updateItem(item: ItemEntity)
}

View File

@ -1,188 +1,229 @@
package bou.amine.apps.readerforselfossv2.rest package bou.amine.apps.readerforselfossv2.rest
import android.content.Context import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import android.os.Parcel
import android.os.Parcelable
import android.text.Html
import bou.amine.apps.readerforselfossv2.rest.SelfossModel.SelfossModel.constructUrl
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.features.* import io.ktor.client.call.*
import io.ktor.client.features.json.* import io.ktor.client.engine.*
import io.ktor.client.features.json.serializer.* import io.ktor.client.engine.ProxyBuilder.http
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
import io.ktor.http.* import io.ktor.http.*
import kotlinx.coroutines.coroutineScope import io.ktor.client.plugins.contentnegotiation.*
import kotlinx.coroutines.launch import io.ktor.client.plugins.logging.*
import kotlinx.serialization.Serializable import io.ktor.serialization.kotlinx.json.*
import kotlin.jvm.JvmField import kotlinx.serialization.json.Json
class SelfossApi { class SelfossApi(private val apiDetailsService: ApiDetailsService) {
/**
* TODO:
* Self signed certs
* Timeout + 408
* Auth digest/basic
* Loging
*/
val baseUrl = "http://10.0.2.2:8888" private val client = HttpClient() {
val userName = "" install(ContentNegotiation) {
val password = "" json(Json {
val client = HttpClient() {
install(JsonFeature) {
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
prettyPrint = true prettyPrint = true
isLenient = true isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
}) })
} }
install(Logging) {
logger = object: Logger {
override fun log(message: String) {
apiDetailsService.logApiCalls(message)
}
}
level = LogLevel.ALL
}
/* TODO: Auth as basic
if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) {
install(Auth) {
basic {
credentials {
BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword())
}
sendWithoutRequest {
true
}
}
}
}*/
expectSuccess = false
} }
private fun url(path: String) = private fun url(path: String) =
"$baseUrl$path" "${apiDetailsService.getBaseUrl()}$path"
suspend fun login() = suspend fun login(): SelfossModel.SuccessResponse? =
client.get<String>(url("/login"))// Todo: params client.get(url("/login")) {
parameter("username", apiDetailsService.getUserName())
parameter("password", apiDetailsService.getPassword())
}.body()
suspend fun getItems(type: String, suspend fun getItems(
items: Int, type: String,
offset: Int, items: Int,
tag: String? = "", offset: Int,
source: Long? = null, tag: String? = "",
search: String? = "", source: Long? = null,
updatedSince: String? = ""): List<SelfossModel.Item> = search: String? = "",
updatedSince: String? = ""
): List<SelfossModel.Item>? =
client.get(url("/items")) { client.get(url("/items")) {
parameter("username", userName) parameter("username", apiDetailsService.getUserName())
parameter("password", password) parameter("password", apiDetailsService.getPassword())
parameter("type", type) parameter("type", type)
parameter("tag", tag) parameter("tag", tag)
parameter("source", source) parameter("source", source)
parameter("search", search) parameter("search", search)
parameter("updatedsince", updatedSince) parameter("updatedsince", updatedSince)
parameter("items", items) parameter("items", items)
parameter("offset", offset) parameter("offset", offset)
} }.body()
suspend fun stats(): SelfossModel.Stats = suspend fun stats(): SelfossModel.Stats? =
client.get(url("/stats")) { client.get(url("/stats")) {
parameter("username", userName) parameter("username", apiDetailsService.getUserName())
parameter("password", password) parameter("password", apiDetailsService.getPassword())
} }.body()
suspend fun tags(): List<SelfossModel.Tag> = suspend fun tags(): List<SelfossModel.Tag>? =
client.get(url("/tags")) { client.get(url("/tags")) {
parameter("username", userName) parameter("username", apiDetailsService.getUserName())
parameter("password", password) parameter("password", apiDetailsService.getPassword())
} }.body()
suspend fun update(): String = suspend fun update(): SelfossModel.SuccessResponse? =
client.get(url("/update")) { client.get(url("/update")) {
parameter("username", userName) parameter("username", apiDetailsService.getUserName())
parameter("password", password) parameter("password", apiDetailsService.getPassword())
} }.body()
suspend fun spouts(): Map<String, SelfossModel.Spout> = suspend fun spouts(): Map<String, SelfossModel.Spout>? =
client.get(url("/sources/spouts")) { client.get(url("/a/spouts")) {
parameter("username", userName) parameter("username", apiDetailsService.getUserName())
parameter("password", password) parameter("password", apiDetailsService.getPassword())
} }.body()
suspend fun sources(): List<SelfossModel.Source> = suspend fun sources(): ArrayList<SelfossModel.Source>? =
client.get(url("/sources/list")) { client.get(url("/sources/list")) {
parameter("username", userName) parameter("username", apiDetailsService.getUserName())
parameter("password", password) parameter("password", apiDetailsService.getPassword())
}.body()
suspend fun version(): SelfossModel.ApiVersion? =
client.get(url("/api/about")).body()
suspend fun markAsRead(id: String): SelfossModel.SuccessResponse? =
client.submitForm(
url = url("/mark/$id"),
formParameters = Parameters.build {
append("username", apiDetailsService.getUserName())
append("password", apiDetailsService.getPassword())
},
encodeInQuery = true
).body()
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse? =
client.submitForm(
url = url("/unmark/$id"),
formParameters = Parameters.build {
append("username", apiDetailsService.getUserName())
append("password", apiDetailsService.getPassword())
},
encodeInQuery = true
).body()
suspend fun starr(id: String): SelfossModel.SuccessResponse? =
client.submitForm(
url = url("/starr/$id"),
formParameters = Parameters.build {
append("username", apiDetailsService.getUserName())
append("password", apiDetailsService.getPassword())
},
encodeInQuery = true
).body()
suspend fun unstarr(id: String): SelfossModel.SuccessResponse? =
client.submitForm(
url = url("/unstarr/$id"),
formParameters = Parameters.build {
append("username", apiDetailsService.getUserName())
append("password", apiDetailsService.getPassword())
},
encodeInQuery = true
).body()
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse? =
client.submitForm(
url = url("/mark"),
formParameters = Parameters.build {
append("username", apiDetailsService.getUserName())
append("password", apiDetailsService.getPassword())
append("ids[]", ids.joinToString(","))
},
encodeInQuery = true
).body()
suspend fun createSourceForVersion(
title: String,
url: String,
spout: String,
tags: String,
filter: String,
version: Int
): SelfossModel.SuccessResponse? =
if (version > 1) {
createSource(title, url, spout, tags, filter)
} else {
createSource2(title, url, spout, tags, filter)
} }
suspend fun version(): SelfossModel.ApiVersion = private suspend fun createSource(
client.get(url("/api/about")) title: String,
url: String,
suspend fun markAsRead(id: String): SelfossModel.SuccessResponse = spout: String,
tags: String,
filter: String
): SelfossModel.SuccessResponse? =
client.submitForm( client.submitForm(
url = url("/mark/$id"), url = url("/source"),
formParameters = Parameters.build { formParameters = Parameters.build {
append("username", userName) append("username", apiDetailsService.getUserName())
append("password", password) append("password", apiDetailsService.getPassword())
}, append("title", title)
encodeInQuery = true append("url", url)
) append("spout", spout)
append("tags", tags)
append("filter", filter)
},
encodeInQuery = true
).body()
suspend fun unmarkAsRead(id: String): SelfossModel.SuccessResponse = private suspend fun createSource2(
title: String,
url: String,
spout: String,
tags: String,
filter: String
): SelfossModel.SuccessResponse? =
client.submitForm( client.submitForm(
url = url("/unmark/$id"), url = url("/source"),
formParameters = Parameters.build { formParameters = Parameters.build {
append("username", userName) append("username", apiDetailsService.getUserName())
append("password", password) append("password", apiDetailsService.getPassword())
}, append("title", title)
encodeInQuery = true append("url", url)
) append("spout", spout)
append("tags[]", tags)
append("filter", filter)
},
encodeInQuery = true
).body()
suspend fun starr(id: String): SelfossModel.SuccessResponse = suspend fun deleteSource(id: String): SelfossModel.SuccessResponse? =
client.submitForm( client.delete(url("/source/$id")) {
url = url("/starr/$id"), parameter("username", apiDetailsService.getUserName())
formParameters = Parameters.build { parameter("password", apiDetailsService.getPassword())
append("username", userName) }.body()
append("password", password)
},
encodeInQuery = true
)
suspend fun unstarr(id: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/unstarr/$id"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
},
encodeInQuery = true
)
suspend fun markAllAsRead(ids: List<String>): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/mark"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
append("ids[]", ids.joinToString(","))
},
encodeInQuery = true
)
suspend fun createSource(title: String, url: String, spout: String, tags: String, filter: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/source"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
append("title", title)
append("url", url)
append("spout", spout)
append("tags", tags)
append("filter", filter)
},
encodeInQuery = true
)
suspend fun createSource2(title: String, url: String, spout: String, tags: String, filter: String): SelfossModel.SuccessResponse =
client.submitForm(
url = url("/source"),
formParameters = Parameters.build {
append("username", userName)
append("password", password)
append("title", title)
append("url", url)
append("spout", spout)
append("tags[]", tags)
append("filter", filter)
},
encodeInQuery = true
)
suspend fun deleteSource(id: String) =
client.delete<SelfossModel.SuccessResponse>(url("/source/$id")) {
parameter("username", userName)
parameter("password", password)
}
} }

View File

@ -1,15 +1,8 @@
package bou.amine.apps.readerforselfossv2.rest package bou.amine.apps.readerforselfossv2.rest
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.text.Html import android.text.Html
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.util.*
import kotlin.collections.ArrayList
import kotlin.jvm.JvmField
import org.jsoup.Jsoup
import java.util.Locale.US
class SelfossModel { class SelfossModel {
@ -61,19 +54,11 @@ class SelfossModel {
data class Source( data class Source(
val id: String, val id: String,
val title: String, val title: String,
val tags: String, val tags: List<String>,
val spout: String, val spout: String,
val error: String, val error: String,
val icon: String val icon: String
) { )
fun getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
}
@Serializable @Serializable
data class Item( data class Item(
@ -88,81 +73,5 @@ class SelfossModel {
val link: String, val link: String,
val sourcetitle: String, val sourcetitle: String,
val tags: String val tags: String
) { )
fun getIcon(baseUrl: String): String {
return constructUrl(baseUrl, "favicons", icon)
}
fun getThumbnail(baseUrl: String): String {
return constructUrl(baseUrl, "thumbnails", thumbnail)
}
fun getImages() : ArrayList<String> {
val allImages = ArrayList<String>()
for ( image in Jsoup.parse(content).getElementsByTag("img")) {
val url = image.attr("src")
if (url.lowercase(US).contains(".jpg") ||
url.lowercase(US).contains(".jpeg") ||
url.lowercase(US).contains(".png") ||
url.lowercase(US).contains(".webp"))
{
allImages.add(url)
}
}
return allImages
}
fun getTitleDecoded(): String {
return Html.fromHtml(title).toString()
}
fun getSourceTitle(): String {
return Html.fromHtml(sourcetitle).toString()
}
// TODO: maybe find a better way to handle these kind of urls
fun getLinkDecoded(): String {
var stringUrl: String
stringUrl =
if (link.startsWith("http://news.google.com/news/") || link.startsWith("https://news.google.com/news/")) {
if (link.contains("&amp;url=")) {
link.substringAfter("&amp;url=")
} else {
this.link.replace("&amp;", "&")
}
} else {
this.link.replace("&amp;", "&")
}
// handle :443 => https
if (stringUrl.contains(":443")) {
stringUrl = stringUrl.replace(":443", "").replace("http://", "https://")
}
// handle url not starting with http
if (stringUrl.startsWith("//")) {
stringUrl = "http:$stringUrl"
}
return stringUrl
}
}
companion object SelfossModel {
private fun String?.isEmptyOrNullOrNullString(): Boolean =
this == null || this == "null" || this.isEmpty()
fun constructUrl(baseUrl: String, path: String, file: String?): String {
return if (file.isEmptyOrNullOrNullString()) {
""
} else {
val baseUriBuilder = Uri.parse(baseUrl).buildUpon()
baseUriBuilder.appendPath(path).appendPath(file)
baseUriBuilder.toString()
}
}
}
} }

View File

@ -0,0 +1,9 @@
package bou.amine.apps.readerforselfossv2.service
interface ApiDetailsService {
fun logApiCalls(message: String)
fun getApiVersion(): Int
fun getBaseUrl(): String
fun getUserName(): String
fun getPassword(): String
}

View File

@ -0,0 +1,34 @@
package bou.amine.apps.readerforselfossv2.service
import bou.amine.apps.readerforselfossv2.dao.DeviceDatabase
import bou.amine.apps.readerforselfossv2.utils.parseDate
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
abstract class DeviceDataBaseService<ItemEntity>(val db: DeviceDatabase<ItemEntity>, private val searchService: SearchService) {
var itemsCaching = false
var items: ArrayList<SelfossModel.Item> = arrayListOf()
get() {
return ArrayList(field)
}
set(value) {
field = ArrayList(value)
}
abstract suspend fun updateDatabase()
abstract suspend fun clearDBItems()
abstract fun appendNewItems(items: List<SelfossModel.Item>)
abstract fun getFromDB()
fun sortItems() {
val tmpItems = ArrayList(items.sortedByDescending { it.parseDate(searchService.dateUtils) })
items = tmpItems
}
// This filtered items from items val. Do not use
fun getFocusedItems() {}
fun computeBadges() {
searchService.badgeUnread = items.filter { item -> item.unread == 1 }.size
searchService.badgeStarred = items.filter { item -> item.starred == 1 }.size
searchService.badgeAll = items.size
}
}

View File

@ -0,0 +1,31 @@
package bou.amine.apps.readerforselfossv2.service
import bou.amine.apps.readerforselfossv2.utils.DateUtils
class SearchService(val dateUtils: DateUtils) {
var displayedItems: String = "unread"
set(value) {
field = when (value) {
"all" -> "all"
"unread" -> "unread"
"read" -> "read"
"starred" -> "starred"
else -> "all"
}
}
var position = 0
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
}

View File

@ -0,0 +1,152 @@
package bou.amine.apps.readerforselfossv2.service
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import kotlinx.coroutines.*
class SelfossService<ItemEntity>(val api: SelfossApi, private val dbService: DeviceDataBaseService<ItemEntity>, private val searchService: SearchService) {
suspend fun getAndStoreAllItems(isNetworkAvailable: Boolean) = withContext(
Dispatchers.Default) {
if (isNetworkAvailable) {
launch {
try {
enqueueArticles(allNewItems(), true)
} catch (e: Throwable) {}
}
launch {
try {
enqueueArticles(allReadItems(), false)
} catch (e: Throwable) {}
}
launch {
try {
enqueueArticles(allStarredItems(), false)
} catch (e: Throwable) {}
}
} else {
launch { dbService.updateDatabase() }
}
}
suspend fun refreshFocusedItems(itemsNumber: Int, isNetworkAvailable: Boolean) = withContext(
Dispatchers.Default) {
if (isNetworkAvailable) {
val response = when (searchService.displayedItems) {
"read" -> readItems(itemsNumber, 0)
"unread" -> newItems(itemsNumber, 0)
"starred" -> starredItems(itemsNumber, 0)
else -> readItems(itemsNumber, 0)
}
if (response != null) {
// TODO:
// dbService.refreshFocusedItems(response.body() as ArrayList<SelfossModel.Item>)
dbService.updateDatabase()
}
}
}
suspend fun getReadItems(itemsNumber: Int, offset: Int, isNetworkAvailable: Boolean) = withContext(
Dispatchers.Default) {
if (isNetworkAvailable) {
try {
enqueueArticles(readItems( itemsNumber, offset), false)
searchService.fetchedAll = true
dbService.updateDatabase()
} catch (e: Throwable) {}
}
}
suspend fun getUnreadItems(itemsNumber: Int, offset: Int, isNetworkAvailable: Boolean) = withContext(
Dispatchers.Default) {
if (isNetworkAvailable) {
try {
if (!searchService.fetchedUnread) {
dbService.clearDBItems()
}
enqueueArticles(newItems(itemsNumber, offset), false)
searchService.fetchedUnread = true
} catch (e: Throwable) {}
}
dbService.updateDatabase()
}
suspend fun getStarredItems(itemsNumber: Int, offset: Int, isNetworkAvailable: Boolean) = withContext(
Dispatchers.Default) {
if (isNetworkAvailable) {
try {
enqueueArticles(starredItems(itemsNumber, offset), false)
searchService.fetchedStarred = true
dbService.updateDatabase()
} catch (e: Throwable) {
}
}
}
suspend fun readAll(isNetworkAvailable: Boolean): Boolean {
var success = false
if (isNetworkAvailable) {
// Do api call to read all
} else {
// Do db call to read all
}
// refresh view
return success
}
suspend fun reloadBadges(isNetworkAvailable: Boolean) = withContext(Dispatchers.Default) {
if (isNetworkAvailable) {
try {
val response = api.stats()
if (response != null) {
searchService.badgeUnread = response.unread
searchService.badgeAll = response.total
searchService.badgeStarred = response.starred
}
} catch (e: Throwable) {}
} else {
dbService.computeBadges()
}
}
private fun enqueueArticles(response: List<SelfossModel.Item>?, clearDatabase: Boolean) {
if (response != null) {
if (clearDatabase) {
CoroutineScope(Dispatchers.Default).launch {
dbService.clearDBItems()
}
}
dbService.appendNewItems(response)
}
}
private suspend fun allNewItems(): List<SelfossModel.Item>? =
readItems(200, 0)
private suspend fun allReadItems(): List<SelfossModel.Item>? =
newItems(200, 0)
private suspend fun allStarredItems(): List<SelfossModel.Item>? =
starredItems(200, 0)
private suspend fun readItems(
itemsNumber: Int,
offset: Int
): List<SelfossModel.Item>? =
api.getItems("read", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter)
private suspend fun newItems(
itemsNumber: Int,
offset: Int
): List<SelfossModel.Item>? =
api.getItems("unread", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter)
private suspend fun starredItems(
itemsNumber: Int,
offset: Int
): List<SelfossModel.Item>? =
api.getItems("starred", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter)
}

View File

@ -0,0 +1,41 @@
package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
fun SelfossModel.Item.parseDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): Instant =
dateUtils.parseDate(this.datetime)
fun SelfossModel.Item.parseRelativeDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): String =
dateUtils.parseRelativeDate(this.datetime)
class DateUtils(private val apiDetailsService: ApiDetailsService) {
fun parseDate(dateString: String): Instant {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (apiDetailsService.getApiVersion() >= 4) {
OffsetDateTime.parse(dateString).toInstant()
} else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)
}
}
fun parseRelativeDate(dateString: String): String {
val date = parseDate(dateString)
return " " + DateUtils.getRelativeTimeSpanString(
date.toEpochMilli(),
Instant.now().toEpochMilli(),
60000L, // DateUtils.MINUTE_IN_MILLIS,
262144 // DateUtils.FORMAT_ABBREV_RELATIVE
)
}
}

View File

@ -0,0 +1,22 @@
package bou.amine.apps.readerforselfossv2.utils
fun String?.isEmptyOrNullOrNullString(): Boolean =
this == null || this == "null" || this.isEmpty()
fun String.longHash(): Long {
var h = 98764321261L
val l = this.length
val chars = this.toCharArray()
for (i in 0 until l) {
h = 31 * h + chars[i].code.toLong()
}
return h
}
fun String.toStringUriWithHttp(): String =
if (!this.startsWith("https://") && !this.startsWith("http://")) {
"http://" + this
} else {
this
}