WIP. Everything isn't working yet, but api calls are made.
This commit is contained in:
parent
bd4d20e858
commit
6e38e8753c
5
BREAKING.md
Normal file
5
BREAKING.md
Normal 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
10
TODO.md
Normal file
@ -0,0 +1,10 @@
|
||||
# TODO
|
||||
|
||||
|
||||
- Service injection
|
||||
|
||||
- Basic auth
|
||||
* Self signed certs
|
||||
* Timeout + 408
|
||||
|
||||
- Clean HTTP login
|
@ -14,9 +14,6 @@ import android.widget.ProgressBar
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
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.Toppings
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
@ -26,10 +23,19 @@ import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
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() {
|
||||
|
||||
private lateinit var apiDetailsService: ApiDetailsService
|
||||
private var mSpoutsValue: String? = null
|
||||
private lateinit var api: SelfossApi
|
||||
|
||||
@ -74,11 +80,13 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val settings =
|
||||
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
apiDetailsService = AndroidApiDetailsService(this@AddSourceActivity)
|
||||
api = SelfossApi(
|
||||
this,
|
||||
this@AddSourceActivity,
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
// this,
|
||||
// this@AddSourceActivity,
|
||||
// settings.getBoolean("isSelfSignedCert", false),
|
||||
// prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
apiDetailsService
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
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 }
|
||||
for ((key, value) in items) {
|
||||
spoutsKV[value.name] = key
|
||||
}
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var items = api!!.spouts()
|
||||
if (items != null) {
|
||||
|
||||
mProgress.visibility = View.GONE
|
||||
formContainer.visibility = View.VISIBLE
|
||||
|
||||
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()
|
||||
val itemsStrings = items.map { it.value.name }
|
||||
for ((key, value) in items) {
|
||||
spoutsKV[value.name] = key
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Map<String, Spout>>, t: Throwable) {
|
||||
handleProblemWithSpouts()
|
||||
}
|
||||
mProgress.visibility = View.GONE
|
||||
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(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_get_spouts,
|
||||
@ -166,7 +161,7 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
).show()
|
||||
mProgress.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeGetDetailsFromIntentSharing(
|
||||
@ -196,70 +191,26 @@ class AddSourceActivity : AppCompatActivity() {
|
||||
sourceDetailsUnavailable -> {
|
||||
Toast.makeText(this, R.string.form_not_complete, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(this).getInt("apiVersionMajor", 0) > 1 -> {
|
||||
val tagList = tags.text.toString().split(",").map { it.trim() }
|
||||
api.createSourceApi2(
|
||||
title,
|
||||
url,
|
||||
mSpoutsValue!!,
|
||||
tagList,
|
||||
""
|
||||
).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
if (response.body() != null && response.body()!!.isSuccess) {
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
else -> {
|
||||
api.createSource(
|
||||
title,
|
||||
url,
|
||||
mSpoutsValue!!,
|
||||
tags.text.toString(),
|
||||
""
|
||||
).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
if (response.body() != null && response.body()!!.isSuccess) {
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response: SelfossModel.SuccessResponse? = api.createSourceForVersion(
|
||||
title,
|
||||
url,
|
||||
mSpoutsValue!!,
|
||||
tags.text.toString(),
|
||||
"",
|
||||
PreferenceManager.getDefaultSharedPreferences(this@AddSourceActivity).getInt("apiVersionMajor", 0)
|
||||
)
|
||||
if (response != null) {
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@AddSourceActivity,
|
||||
R.string.cant_create_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,27 +29,37 @@ import androidx.work.WorkManager
|
||||
import bou.amine.apps.readerforselfossv2.android.adapters.ItemCardAdapter
|
||||
import bou.amine.apps.readerforselfossv2.android.adapters.ItemListAdapter
|
||||
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.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.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_2_3
|
||||
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.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
|
||||
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.removeBadge
|
||||
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.isNetworkAccessible
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
|
||||
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.BottomNavigationItem
|
||||
import com.ashokvarma.bottomnavigation.TextBadgeItem
|
||||
@ -73,14 +83,16 @@ import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
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 DRAWER_ID_TAGS = 100101L
|
||||
private val DRAWER_ID_HIDDEN_TAGS = 101100L
|
||||
@ -90,8 +102,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
private val READ_SHOWN = 2
|
||||
private val FAV_SHOWN = 3
|
||||
|
||||
private var items: ArrayList<Item> = ArrayList()
|
||||
private var allItems: ArrayList<Item> = ArrayList()
|
||||
private var items: ArrayList<SelfossModel.Item> = ArrayList()
|
||||
private var allItems: ArrayList<SelfossModel.Item> = ArrayList()
|
||||
|
||||
private var internalBrowser = false
|
||||
private var articleViewer = false
|
||||
@ -105,7 +117,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
private var displayAccountHeader: Boolean = false
|
||||
private var infiniteScroll: Boolean = false
|
||||
private var lastFetchDone: Boolean = false
|
||||
private var itemsCaching: Boolean = false
|
||||
private var updateSources: Boolean = true
|
||||
private var markOnScroll: Boolean = false
|
||||
private var hiddenTags: List<String> = emptyList()
|
||||
@ -140,7 +151,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
|
||||
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() {
|
||||
super.onStart()
|
||||
@ -184,12 +195,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
settings = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
|
||||
apiDetailsService = AndroidApiDetailsService(applicationContext)
|
||||
api = SelfossApi(
|
||||
this,
|
||||
this@HomeActivity,
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
sharedPref.getString("api_timeout", "-1")!!.toLong()
|
||||
// this,
|
||||
// this@HomeActivity,
|
||||
// settings.getBoolean("isSelfSignedCert", false),
|
||||
// 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()
|
||||
allItems = ArrayList()
|
||||
|
||||
@ -217,7 +235,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
lastFetchDone = false
|
||||
handleDrawerItems()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
refreshFocusedItems(applicationContext, api, db, itemsNumber)
|
||||
service.refreshFocusedItems(itemsNumber, applicationContext.isNetworkAvailable())
|
||||
getElementsAccordingToTab()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
@ -258,7 +276,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
|
||||
reloadBadgeContent()
|
||||
|
||||
val tagHashes = i.tags.tags.split(",").map { it.longHash() }
|
||||
val tagHashes = i.tags.split(",").map { it.longHash() }
|
||||
tagsBadge = tagsBadge.map {
|
||||
if (tagHashes.contains(it.key)) {
|
||||
(it.key to (it.value - 1))
|
||||
@ -334,21 +352,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
}
|
||||
|
||||
private fun getApiMajorVersion() {
|
||||
api.apiVersion.enqueue(object : Callback<ApiVersion> {
|
||||
override fun onFailure(call: Call<ApiVersion>, t: Throwable) {
|
||||
Config.apiVersion = apiVersionMajor
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val version = api.version()
|
||||
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() {
|
||||
@ -382,17 +392,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
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() {
|
||||
super.onStop()
|
||||
customTabActivityHelper.unbindCustomTabsService(this)
|
||||
@ -409,8 +408,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
userIdentifier = sharedPref.getString("unique_id", "")!!
|
||||
displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false)
|
||||
infiniteScroll = sharedPref.getBoolean("infinite_loading", false)
|
||||
itemsCaching = sharedPref.getBoolean("items_caching", false)
|
||||
SharedItems.itemsCaching = itemsCaching
|
||||
searchService.itemsCaching = sharedPref.getBoolean("items_caching", false)
|
||||
updateSources = sharedPref.getBoolean("update_sources", true)
|
||||
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false)
|
||||
hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) {
|
||||
@ -525,7 +523,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
private fun handleDrawerItems() {
|
||||
tagsBadge = emptyMap()
|
||||
fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) {
|
||||
fun handleTags(maybeTags: List<Tag>?) {
|
||||
fun handleTags(maybeTags: List<SelfossModel.Tag>?) {
|
||||
if (maybeTags == null) {
|
||||
if (loadedFromCache) {
|
||||
binding.mainDrawer.itemAdapter.add(
|
||||
@ -560,9 +558,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
color = ColorHolder.fromColor(appColors.colorAccent) }
|
||||
onDrawerItemClickListener = { _,_,_ ->
|
||||
allItems = ArrayList()
|
||||
SharedItems.tagFilter = it.tag
|
||||
SharedItems.sourceFilter = null
|
||||
SharedItems.sourceIDFilter = null
|
||||
searchService.tagFilter = it.tag
|
||||
searchService.sourceFilter = null
|
||||
searchService.sourceIDFilter = null
|
||||
getElementsAccordingToTab()
|
||||
fetchOnEmptyList()
|
||||
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 (loadedFromCache) {
|
||||
binding.mainDrawer.itemAdapter.add(
|
||||
@ -589,7 +587,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val filteredHiddenTags: List<Tag> =
|
||||
val filteredHiddenTags: List<SelfossModel.Tag> =
|
||||
maybeTags.filter { hiddenTags.contains(it.tag) }
|
||||
tagsBadge = filteredHiddenTags.map {
|
||||
val gd = GradientDrawable()
|
||||
@ -613,9 +611,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
color = ColorHolder.fromColor(appColors.colorAccent) }
|
||||
onDrawerItemClickListener = { _,_,_ ->
|
||||
allItems = ArrayList()
|
||||
SharedItems.tagFilter = it.tag
|
||||
SharedItems.sourceFilter = null
|
||||
SharedItems.sourceIDFilter = null
|
||||
searchService.tagFilter = it.tag
|
||||
searchService.sourceFilter = null
|
||||
searchService.sourceIDFilter = null
|
||||
getElementsAccordingToTab()
|
||||
fetchOnEmptyList()
|
||||
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 (loadedFromCache) {
|
||||
binding.mainDrawer.itemAdapter.add(
|
||||
@ -646,12 +644,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
val item = PrimaryDrawerItem().apply {
|
||||
nameText = source.getTitleDecoded()
|
||||
identifier = source.id.toLong()
|
||||
iconUrl = source.getIcon(this@HomeActivity)
|
||||
iconUrl = source.getIcon(apiDetailsService.getBaseUrl())
|
||||
onDrawerItemClickListener = { _,_,_ ->
|
||||
allItems = ArrayList()
|
||||
SharedItems.sourceIDFilter = source.id.toLong()
|
||||
SharedItems.sourceFilter = source.title
|
||||
SharedItems.tagFilter = null
|
||||
searchService.sourceIDFilter = source.id.toLong()
|
||||
searchService.sourceFilter = source.title
|
||||
searchService.tagFilter = null
|
||||
getElementsAccordingToTab()
|
||||
fetchOnEmptyList()
|
||||
false
|
||||
@ -672,9 +670,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
badgeRes = R.string.drawer_action_clear
|
||||
onDrawerItemClickListener = { _,_,_ ->
|
||||
allItems = ArrayList()
|
||||
SharedItems.sourceFilter = null
|
||||
SharedItems.sourceIDFilter = null
|
||||
SharedItems.tagFilter = null
|
||||
searchService.sourceFilter = null
|
||||
searchService.sourceIDFilter = null
|
||||
searchService.tagFilter = null
|
||||
binding.mainDrawer.setSelectionAtPosition(-1)
|
||||
getElementsAccordingToTab()
|
||||
fetchOnEmptyList()
|
||||
@ -769,47 +767,37 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
}
|
||||
|
||||
fun drawerApiCalls(maybeDrawerData: DrawerData?) {
|
||||
var tags: List<Tag>? = null
|
||||
var sources: List<Source>?
|
||||
var tags: List<SelfossModel.Tag>? = null
|
||||
var sources: List<SelfossModel.Source>?
|
||||
|
||||
fun sourcesApiCall() {
|
||||
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) {
|
||||
api.sources.enqueue(object : Callback<List<Source>> {
|
||||
override fun onResponse(
|
||||
call: Call<List<Source>>?,
|
||||
response: Response<List<Source>>
|
||||
) {
|
||||
sources = response.body()
|
||||
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val response = api.sources()
|
||||
if (response != null) {
|
||||
sources = response
|
||||
val apiDrawerData = DrawerData(tags, sources)
|
||||
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
|
||||
handleDrawerData(apiDrawerData)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Source>>?, t: Throwable?) {
|
||||
} else {
|
||||
val apiDrawerData = DrawerData(tags, null)
|
||||
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
|
||||
handleDrawerData(apiDrawerData)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) {
|
||||
api.tags.enqueue(object : Callback<List<Tag>> {
|
||||
override fun onResponse(
|
||||
call: Call<List<Tag>>,
|
||||
response: Response<List<Tag>>
|
||||
) {
|
||||
tags = response.body()
|
||||
sourcesApiCall()
|
||||
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = api.tags()
|
||||
if (response != null) {
|
||||
tags = response
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Tag>>?, t: Throwable?) {
|
||||
sourcesApiCall()
|
||||
}
|
||||
})
|
||||
sourcesApiCall()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -914,9 +902,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
|
||||
private fun fetchOnEmptyList() {
|
||||
binding.recyclerView.doOnNextLayout {
|
||||
if (SharedItems.focusedItems.size - 1 == getLastVisibleItem()) {
|
||||
getElementsAccordingToTab(true)
|
||||
}
|
||||
// Todo:
|
||||
// if (SharedItems.focusedItems.size - 1 == getLastVisibleItem()) {
|
||||
// getElementsAccordingToTab(true)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -968,11 +957,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
}
|
||||
}
|
||||
|
||||
offset = if (appendResults) {
|
||||
SharedItems.focusedItems.size - 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
// Todo:
|
||||
// offset = if (appendResults) {
|
||||
// SharedItems.focusedItems.size - 1
|
||||
// } else {
|
||||
// 0
|
||||
// }
|
||||
firstVisible = if (appendResults) firstVisible else 0
|
||||
|
||||
doGetAccordingToTab()
|
||||
@ -980,39 +970,41 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
|
||||
private fun getUnRead(appendResults: Boolean = false) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (appendResults || !SharedItems.fetchedUnread) {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
getUnreadItems(applicationContext, api, db, itemsNumber, offset)
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
SharedItems.getUnRead()
|
||||
items = SharedItems.focusedItems
|
||||
// Todo:
|
||||
// if (appendResults || !SharedItems.fetchedUnread) {
|
||||
// binding.swipeRefreshLayout.isRefreshing = true
|
||||
// service.getUnreadItems(itemsNumber, offset, applicationContext.isNetworkAvailable())
|
||||
// binding.swipeRefreshLayout.isRefreshing = false
|
||||
// }
|
||||
// Todo: SharedItems.getUnRead()
|
||||
// Todo: items = SharedItems.focusedItems
|
||||
handleListResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getRead(appendResults: Boolean = false) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (appendResults || !SharedItems.fetchedAll) {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
getReadItems(applicationContext, api, db, itemsNumber, offset)
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
SharedItems.getAll()
|
||||
items = SharedItems.focusedItems
|
||||
// Todo:
|
||||
// if (appendResults || !SharedItems.fetchedAll) {
|
||||
// binding.swipeRefreshLayout.isRefreshing = true
|
||||
// service.getReadItems(itemsNumber, offset, applicationContext.isNetworkAvailable())
|
||||
// binding.swipeRefreshLayout.isRefreshing = false
|
||||
// }
|
||||
// SharedItems.getAll()
|
||||
// items = SharedItems.focusedItems
|
||||
handleListResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStarred(appendResults: Boolean = false) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
if (appendResults || !SharedItems.fetchedStarred) {
|
||||
if (appendResults || !searchService.fetchedStarred) {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
getStarredItems(applicationContext, api, db, itemsNumber, offset)
|
||||
service.getStarredItems(itemsNumber, offset, applicationContext.isNetworkAvailable())
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
SharedItems.getStarred()
|
||||
items = SharedItems.focusedItems
|
||||
// Todo: SharedItems.getStarred()
|
||||
// Todo: items = SharedItems.focusedItems
|
||||
handleListResult()
|
||||
}
|
||||
}
|
||||
@ -1036,6 +1028,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
this,
|
||||
items,
|
||||
api,
|
||||
apiDetailsService,
|
||||
db,
|
||||
customTabActivityHelper,
|
||||
internalBrowser,
|
||||
@ -1043,7 +1036,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
fullHeightCards,
|
||||
appColors,
|
||||
userIdentifier,
|
||||
config
|
||||
config,
|
||||
searchService
|
||||
) {
|
||||
updateItems(it)
|
||||
}
|
||||
@ -1053,13 +1047,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
this,
|
||||
items,
|
||||
api,
|
||||
apiDetailsService,
|
||||
db,
|
||||
customTabActivityHelper,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
userIdentifier,
|
||||
appColors,
|
||||
config
|
||||
config,
|
||||
searchService
|
||||
) {
|
||||
updateItems(it)
|
||||
}
|
||||
@ -1083,7 +1079,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
private fun reloadBadges() {
|
||||
if (displayUnreadCount || displayAllCount) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
reloadBadges(applicationContext, api)
|
||||
service.reloadBadges(applicationContext.isNetworkAvailable())
|
||||
reloadBadgeContent()
|
||||
}
|
||||
}
|
||||
@ -1092,15 +1088,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
private fun reloadBadgeContent() {
|
||||
if (displayUnreadCount) {
|
||||
tabNewBadge
|
||||
.setText(SharedItems.badgeUnread.toString())
|
||||
.setText(searchService.badgeUnread.toString())
|
||||
.maybeShow()
|
||||
}
|
||||
if (displayAllCount) {
|
||||
tabArchiveBadge
|
||||
.setText(SharedItems.badgeAll.toString())
|
||||
.setText(searchService.badgeAll.toString())
|
||||
.maybeShow()
|
||||
tabStarredBadge
|
||||
.setText(SharedItems.badgeStarred.toString())
|
||||
.setText(searchService.badgeStarred.toString())
|
||||
.maybeShow()
|
||||
}
|
||||
}
|
||||
@ -1120,7 +1116,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
|
||||
override fun onQueryTextChange(p0: String?): Boolean {
|
||||
if (p0.isNullOrBlank()) {
|
||||
SharedItems.searchFilter = null
|
||||
searchService.searchFilter = null
|
||||
getElementsAccordingToTab()
|
||||
fetchOnEmptyList()
|
||||
}
|
||||
@ -1128,7 +1124,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
}
|
||||
|
||||
override fun onQueryTextSubmit(p0: String?): Boolean {
|
||||
SharedItems.searchFilter = p0
|
||||
searchService.searchFilter = p0
|
||||
getElementsAccordingToTab()
|
||||
fetchOnEmptyList()
|
||||
return false
|
||||
@ -1158,29 +1154,25 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
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) {
|
||||
api.update().enqueue(object : Callback<String> {
|
||||
override fun onResponse(
|
||||
call: Call<String>,
|
||||
response: Response<String>
|
||||
) {
|
||||
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val status = api.update()
|
||||
if (status != null && status.isSuccess) {
|
||||
Toast.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.refresh_success_response, Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<String>, t: Throwable) {
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@HomeActivity,
|
||||
R.string.refresh_failer_message,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
@ -1192,9 +1184,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
|
||||
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
|
||||
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val success = readAll(applicationContext, api, db)
|
||||
val success = service.readAll(applicationContext.isNetworkAvailable())
|
||||
if (success) {
|
||||
Toast.makeText(
|
||||
this@HomeActivity,
|
||||
@ -1230,13 +1222,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
|
||||
private fun maxItemNumber(): Int =
|
||||
when (elementsShown) {
|
||||
UNREAD_SHOWN -> SharedItems.badgeUnread
|
||||
READ_SHOWN -> SharedItems.badgeAll
|
||||
FAV_SHOWN -> SharedItems.badgeStarred
|
||||
else -> SharedItems.badgeUnread // if !elementsShown then unread are fetched.
|
||||
UNREAD_SHOWN -> searchService.badgeUnread
|
||||
READ_SHOWN -> searchService.badgeAll
|
||||
FAV_SHOWN -> searchService.badgeStarred
|
||||
else -> searchService.badgeUnread // if !elementsShown then unread are fetched.
|
||||
}
|
||||
|
||||
private fun updateItems(adapterItems: ArrayList<Item>) {
|
||||
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
|
||||
items = adapterItems
|
||||
}
|
||||
|
||||
@ -1259,32 +1251,24 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
|
||||
}
|
||||
|
||||
private fun handleOfflineActions() {
|
||||
fun <T>doAndReportOnFail(call: Call<T>, action: ActionEntity) {
|
||||
call.enqueue(object: Callback<T> {
|
||||
override fun onResponse(
|
||||
call: Call<T>,
|
||||
response: Response<T>
|
||||
) {
|
||||
thread {
|
||||
db.actionsDao().delete(action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
}
|
||||
})
|
||||
fun doAndReportOnFail(call: SelfossModel.SuccessResponse?, action: ActionEntity) {
|
||||
if (call != null && call.isSuccess) {
|
||||
thread {
|
||||
db.actionsDao().delete(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut)) {
|
||||
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val actions = db.actionsDao().actions()
|
||||
|
||||
actions.forEach { action ->
|
||||
when {
|
||||
action.read -> doAndReportOnFail(api.markItem(action.articleId), action)
|
||||
action.unread -> doAndReportOnFail(api.unmarkItem(action.articleId), action)
|
||||
action.starred -> doAndReportOnFail(api.starrItem(action.articleId), action)
|
||||
action.unstarred -> doAndReportOnFail(api.unstarrItem(action.articleId), action)
|
||||
action.read -> doAndReportOnFail(api.markAsRead(action.articleId), action)
|
||||
action.unread -> doAndReportOnFail(api.unmarkAsRead(action.articleId), action)
|
||||
action.starred -> doAndReportOnFail(api.starr(action.articleId), action)
|
||||
action.unstarred -> doAndReportOnFail(api.unstarr(action.articleId), action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,19 +8,26 @@ import android.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SuccessResponse
|
||||
import androidx.work.Logger
|
||||
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.utils.Config
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
@ -120,6 +127,21 @@ class LoginActivity : AppCompatActivity() {
|
||||
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() {
|
||||
|
||||
// Reset errors.
|
||||
@ -198,45 +220,28 @@ class LoginActivity : AppCompatActivity() {
|
||||
editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert)
|
||||
editor.apply()
|
||||
|
||||
val apiDetailsService = AndroidApiDetailsService(this@LoginActivity)
|
||||
val api = SelfossApi(
|
||||
this,
|
||||
this@LoginActivity,
|
||||
isWithSelfSignedCert,
|
||||
-1L
|
||||
// this,
|
||||
// this@LoginActivity,
|
||||
// isWithSelfSignedCert,
|
||||
// -1L
|
||||
apiDetailsService
|
||||
)
|
||||
|
||||
if (this@LoginActivity.isNetworkAccessible(this@LoginActivity.findViewById(R.id.loginForm))) {
|
||||
api.login().enqueue(object : Callback<SuccessResponse> {
|
||||
private fun preferenceError(t: Throwable) {
|
||||
editor.remove("url")
|
||||
editor.remove("login")
|
||||
editor.remove("httpUserName")
|
||||
editor.remove("password")
|
||||
editor.remove("httpPassword")
|
||||
editor.apply()
|
||||
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) {
|
||||
if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val result = api.login()
|
||||
if (result != null && result.isSuccess) {
|
||||
goToMain()
|
||||
} 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 {
|
||||
showProgress(false)
|
||||
}
|
||||
|
@ -5,29 +5,33 @@ import android.content.SharedPreferences
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.room.Room
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
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.fragments.ArticleFragment
|
||||
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_2_3
|
||||
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.Toppings
|
||||
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.rest.SelfossApi
|
||||
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
|
||||
import com.ftinc.scoop.Scoop
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ReaderActivity : AppCompatActivity() {
|
||||
|
||||
@ -98,10 +102,11 @@ class ReaderActivity : AppCompatActivity() {
|
||||
activeAlignment = prefs.getInt("text_align", JUSTIFY)
|
||||
|
||||
api = SelfossApi(
|
||||
this,
|
||||
this@ReaderActivity,
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
// this,
|
||||
// this@ReaderActivity,
|
||||
// settings.getBoolean("isSelfSignedCert", false),
|
||||
// prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
AndroidApiDetailsService(this@ReaderActivity)
|
||||
)
|
||||
|
||||
if (allItems.isEmpty()) {
|
||||
@ -122,9 +127,11 @@ class ReaderActivity : AppCompatActivity() {
|
||||
binding.indicator.setViewPager(binding.pager)
|
||||
}
|
||||
|
||||
private fun readItem(item: Item) {
|
||||
private fun readItem(item: SelfossModel.Item) {
|
||||
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)
|
||||
toolbarMenu = menu
|
||||
|
||||
if (allItems.isNotEmpty() && allItems[currentItem].starred) {
|
||||
if (allItems.isNotEmpty() && allItems[currentItem].starred == 1) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
@ -187,7 +194,7 @@ class ReaderActivity : AppCompatActivity() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
|
||||
if (allItems[position].starred) {
|
||||
if (allItems[position].starred == 1) {
|
||||
canRemoveFromFavorite()
|
||||
} else {
|
||||
canFavorite()
|
||||
@ -218,21 +225,27 @@ class ReaderActivity : AppCompatActivity() {
|
||||
return true
|
||||
}
|
||||
R.id.star -> {
|
||||
if (allItems[binding.pager.currentItem].starred) {
|
||||
SharedItems.unstarItem(
|
||||
this@ReaderActivity,
|
||||
api,
|
||||
db,
|
||||
allItems[binding.pager.currentItem]
|
||||
)
|
||||
if (allItems[binding.pager.currentItem].starred == 1) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Todo:
|
||||
// SharedItems.unstarItem(
|
||||
// this@ReaderActivity,
|
||||
// api,
|
||||
// db,
|
||||
// allItems[binding.pager.currentItem]
|
||||
// )
|
||||
}
|
||||
afterUnsave()
|
||||
} else {
|
||||
SharedItems.starItem(
|
||||
this@ReaderActivity,
|
||||
api,
|
||||
db,
|
||||
allItems[binding.pager.currentItem]
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Todo:
|
||||
// SharedItems.starItem(
|
||||
// this@ReaderActivity,
|
||||
// api,
|
||||
// db,
|
||||
// allItems[binding.pager.currentItem]
|
||||
// )
|
||||
}
|
||||
afterSave()
|
||||
}
|
||||
}
|
||||
@ -260,6 +273,6 @@ class ReaderActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
companion object {
|
||||
var allItems: ArrayList<Item> = ArrayList()
|
||||
var allItems: ArrayList<SelfossModel.Item> = ArrayList()
|
||||
}
|
||||
}
|
||||
|
@ -9,17 +9,23 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.widget.Toast
|
||||
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.service.AndroidApiDetailsService
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.Toppings
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.ArrayList
|
||||
|
||||
class SourcesActivity : AppCompatActivity() {
|
||||
|
||||
@ -60,27 +66,27 @@ class SourcesActivity : AppCompatActivity() {
|
||||
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
val apiDetailsService = AndroidApiDetailsService(this@SourcesActivity)
|
||||
val api = SelfossApi(
|
||||
this,
|
||||
this@SourcesActivity,
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
// this,
|
||||
// this@SourcesActivity,
|
||||
// settings.getBoolean("isSelfSignedCert", false),
|
||||
// prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
apiDetailsService
|
||||
)
|
||||
var items: ArrayList<Source> = ArrayList()
|
||||
var items: ArrayList<SelfossModel.Source>
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = mLayoutManager
|
||||
|
||||
if (this@SourcesActivity.isNetworkAccessible(binding.recyclerView)) {
|
||||
api.sources.enqueue(object : Callback<List<Source>> {
|
||||
override fun onResponse(
|
||||
call: Call<List<Source>>,
|
||||
response: Response<List<Source>>
|
||||
) {
|
||||
if (response.body() != null && response.body()!!.isNotEmpty()) {
|
||||
items = response.body() as ArrayList<Source>
|
||||
}
|
||||
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api)
|
||||
if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val response = api.sources()
|
||||
if (response != null) {
|
||||
items = response
|
||||
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api,
|
||||
apiDetailsService
|
||||
)
|
||||
binding.recyclerView.adapter = mAdapter
|
||||
mAdapter.notifyDataSetChanged()
|
||||
if (items.isEmpty()) {
|
||||
@ -90,16 +96,14 @@ class SourcesActivity : AppCompatActivity() {
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Source>>, t: Throwable) {
|
||||
} else {
|
||||
Toast.makeText(
|
||||
this@SourcesActivity,
|
||||
R.string.cant_get_sources,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
binding.fab.setOnClickListener {
|
||||
|
@ -2,38 +2,38 @@ package bou.amine.apps.readerforselfossv2.android.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView.ScaleType
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.model.*
|
||||
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
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.*
|
||||
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.circularBitmapDrawable
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openInBrowserAsNewTask
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.shareLink
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.sourceAndDateText
|
||||
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 bou.amine.apps.readerforselfossv2.service.SearchService
|
||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import com.bumptech.glide.Glide
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ItemCardAdapter(
|
||||
override val app: Activity,
|
||||
override var items: ArrayList<Item>,
|
||||
override var items: ArrayList<SelfossModel.Item>,
|
||||
override val api: SelfossApi,
|
||||
override val apiDetailsService: ApiDetailsService,
|
||||
override val db: AppDatabase,
|
||||
private val helper: CustomTabActivityHelper,
|
||||
private val internalBrowser: Boolean,
|
||||
@ -42,7 +42,8 @@ class ItemCardAdapter(
|
||||
override val appColors: AppColors,
|
||||
override val userIdentifier: String,
|
||||
override val config: Config,
|
||||
override val updateItems: (ArrayList<Item>) -> Unit
|
||||
override val searchService: SearchService,
|
||||
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
|
||||
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
|
||||
private val c: Context = app.baseContext
|
||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
@ -58,30 +59,30 @@ class ItemCardAdapter(
|
||||
with(holder) {
|
||||
val itm = items[position]
|
||||
|
||||
binding.favButton.isSelected = itm.starred
|
||||
binding.favButton.isSelected = itm.starred == 1
|
||||
binding.title.text = itm.getTitleDecoded()
|
||||
|
||||
binding.title.setOnTouchListener(LinkOnTouchListener())
|
||||
|
||||
binding.title.setLinkTextColor(appColors.colorAccent)
|
||||
|
||||
binding.sourceTitleAndDate.text = itm.sourceAndDateText()
|
||||
binding.sourceTitleAndDate.text = itm.sourceAndDateText(DateUtils(apiDetailsService))
|
||||
|
||||
if (!fullHeightCards) {
|
||||
binding.itemImage.maxHeight = imageMaxHeight
|
||||
binding.itemImage.scaleType = ScaleType.CENTER_CROP
|
||||
}
|
||||
|
||||
if (itm.getThumbnail(c).isEmpty()) {
|
||||
if (itm.getThumbnail(apiDetailsService.getBaseUrl()).isEmpty()) {
|
||||
binding.itemImage.visibility = View.GONE
|
||||
Glide.with(c).clear(binding.itemImage)
|
||||
binding.itemImage.setImageDrawable(null)
|
||||
} else {
|
||||
binding.itemImage.visibility = View.VISIBLE
|
||||
c.bitmapCenterCrop(config, itm.getThumbnail(c), binding.itemImage)
|
||||
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 drawable =
|
||||
@ -91,7 +92,7 @@ class ItemCardAdapter(
|
||||
.build(itm.getSourceTitle().toTextDrawableString(c), color)
|
||||
binding.sourceImage.setImageDrawable(drawable)
|
||||
} 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 {
|
||||
val item = items[bindingAdapterPosition]
|
||||
if (isNetworkAvailable(c)) {
|
||||
if (item.starred) {
|
||||
SharedItems.unstarItem(c, api, db, item)
|
||||
item.starred = false
|
||||
if (c.isNetworkAvailable()) {
|
||||
if (item.starred == 1) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Todo: SharedItems.unstarItem(c, api, db, item)
|
||||
}
|
||||
item.starred = 0
|
||||
binding.favButton.isSelected = false
|
||||
} else {
|
||||
SharedItems.starItem(c, api, db, item)
|
||||
item.starred = true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Todo: SharedItems.starItem(c, api, db, item)
|
||||
}
|
||||
item.starred = 1
|
||||
binding.favButton.isSelected = true
|
||||
}
|
||||
}
|
||||
@ -145,7 +150,8 @@ class ItemCardAdapter(
|
||||
customTabsIntent,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app
|
||||
app,
|
||||
searchService
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -2,31 +2,30 @@ package bou.amine.apps.readerforselfossv2.android.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.Config
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.LinkOnTouchListener
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.buildCustomTabsIntent
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.*
|
||||
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.circularBitmapDrawable
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.openItemUrl
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.sourceAndDateText
|
||||
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 bou.amine.apps.readerforselfossv2.service.SearchService
|
||||
import bou.amine.apps.readerforselfossv2.utils.DateUtils
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
class ItemListAdapter(
|
||||
override val app: Activity,
|
||||
override var items: ArrayList<Item>,
|
||||
override var items: ArrayList<SelfossModel.Item>,
|
||||
override val api: SelfossApi,
|
||||
override val apiDetailsService: ApiDetailsService,
|
||||
override val db: AppDatabase,
|
||||
private val helper: CustomTabActivityHelper,
|
||||
private val internalBrowser: Boolean,
|
||||
@ -34,7 +33,8 @@ class ItemListAdapter(
|
||||
override val userIdentifier: String,
|
||||
override val appColors: AppColors,
|
||||
override val config: Config,
|
||||
override val updateItems: (ArrayList<Item>) -> Unit
|
||||
override val searchService: SearchService,
|
||||
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
|
||||
) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
|
||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
private val c: Context = app.baseContext
|
||||
@ -54,11 +54,11 @@ class ItemListAdapter(
|
||||
|
||||
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 drawable =
|
||||
@ -69,10 +69,10 @@ class ItemListAdapter(
|
||||
|
||||
binding.itemImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
|
||||
c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage)
|
||||
}
|
||||
} 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,
|
||||
internalBrowser,
|
||||
articleViewer,
|
||||
app
|
||||
app,
|
||||
searchService
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -5,31 +5,37 @@ import android.graphics.Color
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.themes.AppColors
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
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 apiDetailsService: ApiDetailsService
|
||||
abstract val db: AppDatabase
|
||||
abstract val userIdentifier: String
|
||||
abstract val app: Activity
|
||||
abstract val appColors: AppColors
|
||||
abstract val config: Config
|
||||
abstract val updateItems: (ArrayList<Item>) -> Unit
|
||||
abstract val searchService: SearchService
|
||||
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
|
||||
|
||||
fun updateAllItems() {
|
||||
items = SharedItems.focusedItems
|
||||
items = ArrayList() // TODO: SharedItems.focusedItems
|
||||
notifyDataSetChanged()
|
||||
updateItems(items)
|
||||
}
|
||||
|
||||
private fun unmarkSnackbar(i: Item, position: Int) {
|
||||
private fun unmarkSnackbar(i: SelfossModel.Item, position: Int) {
|
||||
val s = Snackbar
|
||||
.make(
|
||||
app.findViewById(R.id.coordLayout),
|
||||
@ -37,12 +43,15 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.undo_string) {
|
||||
SharedItems.unreadItem(app, api, db, i)
|
||||
if (SharedItems.displayedItems == "unread") {
|
||||
addItemAtIndex(i, position)
|
||||
} else {
|
||||
notifyItemChanged(position)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Todo: SharedItems.unreadItem(app, api, db, i)
|
||||
}
|
||||
// Todo:
|
||||
// if (SharedItems.displayedItems == "unread") {
|
||||
// addItemAtIndex(i, position)
|
||||
// } else {
|
||||
// notifyItemChanged(position)
|
||||
// }
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
@ -59,14 +68,17 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.undo_string) {
|
||||
SharedItems.readItem(app, api, db, items[position])
|
||||
items = SharedItems.focusedItems
|
||||
if (SharedItems.displayedItems == "unread") {
|
||||
notifyItemRemoved(position)
|
||||
updateItems(items)
|
||||
} else {
|
||||
notifyItemChanged(position)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Todo: SharedItems.readItem(app, api, db, items[position])
|
||||
}
|
||||
// Todo: items = SharedItems.focusedItems
|
||||
// Todo:
|
||||
// if (SharedItems.displayedItems == "unread") {
|
||||
// notifyItemRemoved(position)
|
||||
// updateItems(items)
|
||||
// } else {
|
||||
// notifyItemChanged(position)
|
||||
// }
|
||||
}
|
||||
|
||||
val view = s.view
|
||||
@ -76,40 +88,46 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
|
||||
}
|
||||
|
||||
fun handleItemAtIndex(position: Int) {
|
||||
if (SharedItems.unreadItemStatusAtIndex(position)) {
|
||||
readItemAtIndex(position)
|
||||
} else {
|
||||
unreadItemAtIndex(position)
|
||||
}
|
||||
// Todo:
|
||||
// if (SharedItems.unreadItemStatusAtIndex(position)) {
|
||||
// readItemAtIndex(position)
|
||||
// } else {
|
||||
// unreadItemAtIndex(position)
|
||||
// }
|
||||
}
|
||||
|
||||
private fun readItemAtIndex(position: Int) {
|
||||
val i = items[position]
|
||||
SharedItems.readItem(app, api, db, i)
|
||||
if (SharedItems.displayedItems == "unread") {
|
||||
items.remove(i)
|
||||
notifyItemRemoved(position)
|
||||
updateItems(items)
|
||||
} else {
|
||||
notifyItemChanged(position)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// Todo: SharedItems.readItem(app, api, db, i)
|
||||
}
|
||||
// Todo:
|
||||
// if (SharedItems.displayedItems == "unread") {
|
||||
// items.remove(i)
|
||||
// notifyItemRemoved(position)
|
||||
// updateItems(items)
|
||||
// } else {
|
||||
// notifyItemChanged(position)
|
||||
// }
|
||||
unmarkSnackbar(i, position)
|
||||
}
|
||||
|
||||
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)
|
||||
markSnackbar(position)
|
||||
}
|
||||
|
||||
fun addItemAtIndex(item: Item, position: Int) {
|
||||
fun addItemAtIndex(item: SelfossModel.Item, position: Int) {
|
||||
items.add(position, item)
|
||||
notifyItemInserted(position)
|
||||
updateItems(items)
|
||||
|
||||
}
|
||||
|
||||
fun addItemsAtEnd(newItems: List<Item>) {
|
||||
fun addItemsAtEnd(newItems: List<SelfossModel.Item>) {
|
||||
val oldSize = items.size
|
||||
items.addAll(newItems)
|
||||
notifyItemRangeInserted(oldSize, newItems.size)
|
||||
|
@ -2,31 +2,34 @@ package bou.amine.apps.readerforselfossv2.android.adapters
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
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.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.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.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.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.util.ColorGenerator
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SourcesListAdapter(
|
||||
private val app: Activity,
|
||||
private val items: ArrayList<Source>,
|
||||
private val api: SelfossApi
|
||||
private val items: ArrayList<SelfossModel.Source>,
|
||||
private val api: SelfossApi,
|
||||
private val apiDetailsService: ApiDetailsService
|
||||
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
|
||||
private val c: Context = app.baseContext
|
||||
private val generator: ColorGenerator = ColorGenerator.MATERIAL
|
||||
@ -42,7 +45,7 @@ class SourcesListAdapter(
|
||||
val itm = items[position]
|
||||
config = Config(c)
|
||||
|
||||
if (itm.getIcon(c).isEmpty()) {
|
||||
if (itm.getIcon(apiDetailsService.getBaseUrl()).isEmpty()) {
|
||||
val color = generator.getColor(itm.getTitleDecoded())
|
||||
|
||||
val drawable =
|
||||
@ -52,7 +55,7 @@ class SourcesListAdapter(
|
||||
.build(itm.getTitleDecoded().toTextDrawableString(c), color)
|
||||
binding.itemImage.setImageDrawable(drawable)
|
||||
} else {
|
||||
c.circularBitmapDrawable(config, itm.getIcon(c), binding.itemImage)
|
||||
c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage)
|
||||
}
|
||||
|
||||
binding.sourceTitle.text = itm.getTitleDecoded()
|
||||
@ -71,34 +74,22 @@ class SourcesListAdapter(
|
||||
val deleteBtn: Button = mView.findViewById(R.id.deleteBtn)
|
||||
|
||||
deleteBtn.setOnClickListener {
|
||||
if (c.isNetworkAccessible(null)) {
|
||||
if (c.isNetworkAvailable(null)) {
|
||||
val (id) = items[adapterPosition]
|
||||
api.deleteSource(id).enqueue(object : Callback<SuccessResponse> {
|
||||
override fun onResponse(
|
||||
call: Call<SuccessResponse>,
|
||||
response: Response<SuccessResponse>
|
||||
) {
|
||||
if (response.body() != null && response.body()!!.isSuccess) {
|
||||
items.removeAt(adapterPosition)
|
||||
notifyItemRemoved(adapterPosition)
|
||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SuccessResponse>, t: Throwable) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val action = api.deleteSource(id)
|
||||
if (action != null && action.isSuccess) {
|
||||
items.removeAt(adapterPosition)
|
||||
notifyItemRemoved(adapterPosition)
|
||||
notifyItemRangeChanged(adapterPosition, itemCount)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
app,
|
||||
R.string.can_delete_source,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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("&url=")) {
|
||||
link.substringAfter("&url=")
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
@ -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>
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -14,22 +14,27 @@ import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import bou.amine.apps.readerforselfossv2.android.MainActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossApi
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.getAndStoreAllItems
|
||||
import bou.amine.apps.readerforselfossv2.android.model.preloadImages
|
||||
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.entities.ActionEntity
|
||||
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.android.service.AndroidApiDetailsService
|
||||
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.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.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
import kotlin.concurrent.thread
|
||||
@ -43,14 +48,20 @@ override fun doWork(): Result {
|
||||
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context)
|
||||
val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false)
|
||||
if (periodicRefresh) {
|
||||
val apiDetailsService = AndroidApiDetailsService(this.context)
|
||||
val api = SelfossApi(
|
||||
this.context,
|
||||
null,
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
sharedPref.getString("api_timeout", "-1")!!.toLong()
|
||||
// this.context,
|
||||
// null,
|
||||
// settings.getBoolean("isSelfSignedCert", false),
|
||||
// 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 {
|
||||
val notificationManager =
|
||||
@ -80,26 +91,26 @@ override fun doWork(): Result {
|
||||
actions.forEach { action ->
|
||||
when {
|
||||
action.read -> doAndReportOnFail(
|
||||
api.markItem(action.articleId),
|
||||
api.markAsRead(action.articleId),
|
||||
action
|
||||
)
|
||||
action.unread -> doAndReportOnFail(
|
||||
api.unmarkItem(action.articleId),
|
||||
api.unmarkAsRead(action.articleId),
|
||||
action
|
||||
)
|
||||
action.starred -> doAndReportOnFail(
|
||||
api.starrItem(action.articleId),
|
||||
api.starr(action.articleId),
|
||||
action
|
||||
)
|
||||
action.unstarred -> doAndReportOnFail(
|
||||
api.unstarrItem(action.articleId),
|
||||
api.unstarr(action.articleId),
|
||||
action
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
getAndStoreAllItems(context, api, db)
|
||||
SharedItems.updateDatabase(db)
|
||||
service.getAndStoreAllItems(context.isNetworkAvailable())
|
||||
// TODO: SharedItems.updateDatabase(db, dateUtils)
|
||||
storeItems(notifyNewItems, notificationManager)
|
||||
}
|
||||
}
|
||||
@ -109,10 +120,10 @@ override fun doWork(): Result {
|
||||
|
||||
private fun storeItems(notifyNewItems: Boolean, notificationManager: NotificationManager) {
|
||||
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) {
|
||||
|
||||
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) {
|
||||
call.enqueue(object : Callback<T> {
|
||||
override fun onResponse(
|
||||
call: Call<T>,
|
||||
response: Response<T>
|
||||
) {
|
||||
thread {
|
||||
db.actionsDao().delete(action)
|
||||
}
|
||||
private fun doAndReportOnFail(result: SelfossModel.SuccessResponse?, action: ActionEntity) {
|
||||
if (result != null && result.isSuccess) {
|
||||
thread {
|
||||
db.actionsDao().delete(action)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -10,39 +10,51 @@ import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.view.*
|
||||
import android.webkit.*
|
||||
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.browser.customtabs.CustomTabsIntent
|
||||
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 bou.amine.apps.readerforselfossv2.android.ImageActivity
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
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.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.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.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.android.service.AndroidApiDetailsService
|
||||
import bou.amine.apps.readerforselfossv2.android.themes.AppColors
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.*
|
||||
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.network.isNetworkAccessible
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.*
|
||||
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
|
||||
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.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
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.Callback
|
||||
import retrofit2.Response
|
||||
@ -50,11 +62,13 @@ import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
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 lateinit var item: Item
|
||||
private lateinit var item: SelfossModel.Item
|
||||
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
|
||||
private lateinit var url: String
|
||||
private lateinit var contentText: String
|
||||
@ -91,6 +105,12 @@ class ArticleFragment : Fragment() {
|
||||
|
||||
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)!!
|
||||
|
||||
db = Room.databaseBuilder(
|
||||
@ -110,8 +130,8 @@ class ArticleFragment : Fragment() {
|
||||
url = item.getLinkDecoded()
|
||||
contentText = item.content
|
||||
contentTitle = item.getTitleDecoded()
|
||||
contentImage = item.getThumbnail(requireActivity())
|
||||
contentSource = item.sourceAndDateText()
|
||||
contentImage = item.getThumbnail(apiDetailsService.getBaseUrl())
|
||||
contentSource = item.sourceAndDateText(DateUtils(apiDetailsService))
|
||||
allImages = item.getImages()
|
||||
|
||||
prefs = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||
@ -136,10 +156,11 @@ class ArticleFragment : Fragment() {
|
||||
val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
|
||||
|
||||
val api = SelfossApi(
|
||||
requireContext(),
|
||||
requireActivity(),
|
||||
settings.getBoolean("isSelfSignedCert", false),
|
||||
prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
// requireContext(),
|
||||
// requireActivity(),
|
||||
// settings.getBoolean("isSelfSignedCert", false),
|
||||
// prefs.getString("api_timeout", "-1")!!.toLong()
|
||||
apiDetailsService
|
||||
)
|
||||
|
||||
fab = binding.fab
|
||||
@ -166,27 +187,33 @@ class ArticleFragment : Fragment() {
|
||||
R.id.share_action -> requireActivity().shareLink(url, contentTitle)
|
||||
R.id.open_action -> requireActivity().openInBrowserAsNewTask(this@ArticleFragment.item)
|
||||
R.id.unread_action -> if (context != null) {
|
||||
if (this@ArticleFragment.item.unread) {
|
||||
SharedItems.readItem(
|
||||
context!!,
|
||||
api,
|
||||
db,
|
||||
this@ArticleFragment.item
|
||||
)
|
||||
this@ArticleFragment.item.unread = false
|
||||
if (this@ArticleFragment.item.unread == 1) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// TODO:
|
||||
// dbService.readItem(
|
||||
// context!!,
|
||||
// api,
|
||||
// db,
|
||||
// this@ArticleFragment.item
|
||||
// )
|
||||
}
|
||||
this@ArticleFragment.item.unread = 0
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.marked_as_read,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
SharedItems.unreadItem(
|
||||
context!!,
|
||||
api,
|
||||
db,
|
||||
this@ArticleFragment.item
|
||||
)
|
||||
this@ArticleFragment.item.unread = true
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
// TODO
|
||||
// .unreadItem(
|
||||
// context!!,
|
||||
// api,
|
||||
// db,
|
||||
// this@ArticleFragment.item
|
||||
// )
|
||||
}
|
||||
this@ArticleFragment.item.unread = 1
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.marked_as_unread,
|
||||
@ -284,7 +311,7 @@ class ArticleFragment : Fragment() {
|
||||
}
|
||||
|
||||
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
|
||||
val parser = MercuryApi()
|
||||
|
||||
@ -535,11 +562,11 @@ class ArticleFragment : Fragment() {
|
||||
private const val ARG_ITEMS = "items"
|
||||
|
||||
fun newInstance(
|
||||
item: Item
|
||||
item: SelfossModel.Item
|
||||
): ArticleFragment {
|
||||
val fragment = ArticleFragment()
|
||||
val args = Bundle()
|
||||
args.putParcelable(ARG_ITEMS, item)
|
||||
args.putParcelable(ARG_ITEMS, item.toParcelable())
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
|
@ -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("&url=")) {
|
||||
link.substringAfter("&url=")
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
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
|
||||
|
||||
|
||||
@ -13,17 +13,17 @@ import androidx.room.Update
|
||||
@Dao
|
||||
interface ItemsDao {
|
||||
@Query("SELECT * FROM items order by id desc")
|
||||
suspend fun items(): List<ItemEntity>
|
||||
suspend fun items(): List<AndroidItemEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertAllItems(vararg items: ItemEntity)
|
||||
suspend fun insertAllItems(vararg items: AndroidItemEntity)
|
||||
|
||||
@Query("DELETE FROM items")
|
||||
suspend fun deleteAllItems()
|
||||
|
||||
@Delete
|
||||
suspend fun delete(item: ItemEntity)
|
||||
suspend fun delete(item: AndroidItemEntity)
|
||||
|
||||
@Update
|
||||
suspend fun updateItem(item: ItemEntity)
|
||||
suspend fun updateItem(item: AndroidItemEntity)
|
||||
}
|
@ -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.ItemsDao
|
||||
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.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 fun drawerDataDao(): DrawerDataDao
|
||||
|
||||
|
@ -3,9 +3,10 @@ package bou.amine.apps.readerforselfossv2.android.persistence.entities
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
|
||||
|
||||
@Entity(tableName = "items")
|
||||
data class ItemEntity(
|
||||
data class AndroidItemEntity(
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "id")
|
||||
val id: String,
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
@ -3,27 +3,7 @@ package bou.amine.apps.readerforselfossv2.android.utils
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
|
||||
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
|
||||
}
|
||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
||||
|
||||
fun Context.shareLink(itemUrl: String, itemTitle: String) {
|
||||
val sendIntent = Intent()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils
|
||||
|
||||
import android.content.Context
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossTagType
|
||||
import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
|
||||
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 {
|
||||
val textDrawable = StringBuilder()
|
||||
@ -15,22 +17,22 @@ fun String.toTextDrawableString(c: Context): String {
|
||||
return textDrawable.toString()
|
||||
}
|
||||
|
||||
fun Item.sourceAndDateText(): String {
|
||||
val formattedDate = parseRelativeDate(this.datetime)
|
||||
fun SelfossModel.Item.sourceAndDateText(dateUtils: DateUtils): String {
|
||||
val formattedDate = parseRelativeDate(dateUtils)
|
||||
|
||||
return this.getSourceTitle() + formattedDate
|
||||
return getSourceTitle() + formattedDate
|
||||
}
|
||||
|
||||
fun Item.toggleStar(): Item {
|
||||
this.starred = !this.starred
|
||||
fun SelfossModel.Item.toggleStar(): SelfossModel.Item {
|
||||
this.starred = if (this.starred == 0) 1 else 0
|
||||
return this
|
||||
}
|
||||
|
||||
fun List<Item>.flattenTags(): List<Item> =
|
||||
fun List<SelfossModel.Item>.flattenTags(): List<SelfossModel.Item> =
|
||||
this.flatMap {
|
||||
val item = it
|
||||
val tags: List<String> = it.tags.tags.split(",")
|
||||
val tags: List<String> = it.tags.split(",")
|
||||
tags.map { t ->
|
||||
item.copy(tags = SelfossTagType(t.trim()))
|
||||
item.copy(tags = t.trim())
|
||||
}
|
||||
}
|
@ -18,8 +18,11 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import bou.amine.apps.readerforselfossv2.android.R
|
||||
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.rest.SelfossModel
|
||||
import bou.amine.apps.readerforselfossv2.service.SearchService
|
||||
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
fun Context.buildCustomTabsIntent(): CustomTabsIntent {
|
||||
@ -71,16 +74,17 @@ fun Context.buildCustomTabsIntent(): CustomTabsIntent {
|
||||
}
|
||||
|
||||
fun Context.openItemUrlInternally(
|
||||
allItems: ArrayList<Item>,
|
||||
allItems: ArrayList<SelfossModel.Item>,
|
||||
currentItem: Int,
|
||||
linkDecoded: String,
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
articleViewer: Boolean,
|
||||
app: Activity
|
||||
app: Activity,
|
||||
searchService: SearchService
|
||||
) {
|
||||
if (articleViewer) {
|
||||
ReaderActivity.allItems = allItems
|
||||
SharedItems.position = currentItem
|
||||
searchService.position = currentItem
|
||||
val intent = Intent(this, ReaderActivity::class.java)
|
||||
intent.putExtra("currentItem", currentItem)
|
||||
app.startActivity(intent)
|
||||
@ -113,13 +117,14 @@ fun Context.openItemUrlInternalBrowser(
|
||||
}
|
||||
|
||||
fun Context.openItemUrl(
|
||||
allItems: ArrayList<Item>,
|
||||
allItems: ArrayList<SelfossModel.Item>,
|
||||
currentItem: Int,
|
||||
linkDecoded: String,
|
||||
customTabsIntent: CustomTabsIntent,
|
||||
internalBrowser: Boolean,
|
||||
articleViewer: Boolean,
|
||||
app: Activity
|
||||
app: Activity,
|
||||
searchService: SearchService
|
||||
) {
|
||||
|
||||
if (!linkDecoded.isUrlValid()) {
|
||||
@ -138,7 +143,8 @@ fun Context.openItemUrl(
|
||||
linkDecoded,
|
||||
customTabsIntent,
|
||||
articleViewer,
|
||||
app
|
||||
app,
|
||||
searchService
|
||||
)
|
||||
} else {
|
||||
this.openItemUrlInternalBrowser(
|
||||
@ -174,7 +180,7 @@ fun String.isBaseUrlValid(ctx: Context): Boolean {
|
||||
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)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
intent.data = Uri.parse(i.getLinkDecoded().toStringUriWithHttp())
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -14,8 +14,11 @@ var snackBarShown = false
|
||||
var view: View? = null
|
||||
lateinit var s: Snackbar
|
||||
|
||||
fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boolean {
|
||||
val networkIsAccessible = isNetworkAvailable(this)
|
||||
fun Context.isNetworkAvailable(
|
||||
v: View? = null,
|
||||
overrideOffline: Boolean = false
|
||||
): Boolean {
|
||||
val networkIsAccessible = isNetworkAccessible(this)
|
||||
|
||||
if (v != null && (!networkIsAccessible || overrideOffline) && (!snackBarShown || v != view)) {
|
||||
view = v
|
||||
@ -43,22 +46,22 @@ fun Context.isNetworkAccessible(v: View?, overrideOffline: Boolean = false): Boo
|
||||
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
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val network = connectivityManager.activeNetwork ?: return false
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
val network = connectivityManager.activeNetwork ?: return false
|
||||
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||
|
||||
return when {
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
else -> false
|
||||
}
|
||||
return when {
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
val network = connectivityManager.activeNetworkInfo ?: return false
|
||||
return network.isConnectedOrConnecting
|
||||
return network.isConnectedOrConnecting
|
||||
}
|
||||
}
|
@ -1,73 +1,72 @@
|
||||
package bou.amine.apps.readerforselfossv2.android.utils.persistence
|
||||
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Item
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.SelfossTagType
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Source
|
||||
import bou.amine.apps.readerforselfossv2.android.api.selfoss.Tag
|
||||
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ItemEntity
|
||||
import bou.amine.apps.readerforselfossv2.android.model.getSourceTitle
|
||||
import bou.amine.apps.readerforselfossv2.android.model.getTitleDecoded
|
||||
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.TagEntity
|
||||
import bou.amine.apps.readerforselfossv2.rest.SelfossModel
|
||||
|
||||
fun TagEntity.toView(): Tag =
|
||||
Tag(
|
||||
fun TagEntity.toView(): SelfossModel.Tag =
|
||||
SelfossModel.Tag(
|
||||
this.tag,
|
||||
this.color,
|
||||
this.unread
|
||||
)
|
||||
|
||||
fun SourceEntity.toView(): Source =
|
||||
Source(
|
||||
fun SourceEntity.toView(): SelfossModel.Source =
|
||||
SelfossModel.Source(
|
||||
this.id,
|
||||
this.title,
|
||||
SelfossTagType(this.tags),
|
||||
this.tags.split(","),
|
||||
this.spout,
|
||||
this.error,
|
||||
this.icon
|
||||
)
|
||||
|
||||
fun Source.toEntity(): SourceEntity =
|
||||
fun SelfossModel.Source.toEntity(): SourceEntity =
|
||||
SourceEntity(
|
||||
this.id,
|
||||
this.getTitleDecoded(),
|
||||
this.tags.tags,
|
||||
this.tags.joinToString(","),
|
||||
this.spout,
|
||||
this.error,
|
||||
this.icon.orEmpty()
|
||||
this.icon
|
||||
)
|
||||
|
||||
fun Tag.toEntity(): TagEntity =
|
||||
fun SelfossModel.Tag.toEntity(): TagEntity =
|
||||
TagEntity(
|
||||
this.tag,
|
||||
this.color,
|
||||
this.unread
|
||||
)
|
||||
|
||||
fun ItemEntity.toView(): Item =
|
||||
Item(
|
||||
fun AndroidItemEntity.toView(): SelfossModel.Item =
|
||||
SelfossModel.Item(
|
||||
this.id,
|
||||
this.datetime,
|
||||
this.title,
|
||||
this.content,
|
||||
this.unread,
|
||||
this.starred,
|
||||
if (this.unread) 1 else 0,
|
||||
if (this.starred) 1 else 0,
|
||||
this.thumbnail,
|
||||
this.icon,
|
||||
this.link,
|
||||
this.sourcetitle,
|
||||
SelfossTagType(this.tags)
|
||||
this.tags
|
||||
)
|
||||
|
||||
fun Item.toEntity(): ItemEntity =
|
||||
ItemEntity(
|
||||
fun SelfossModel.Item.toEntity(): AndroidItemEntity =
|
||||
AndroidItemEntity(
|
||||
this.id,
|
||||
this.datetime,
|
||||
this.getTitleDecoded(),
|
||||
this.content,
|
||||
this.unread,
|
||||
this.starred,
|
||||
this.unread == 1,
|
||||
this.starred == 1,
|
||||
this.thumbnail,
|
||||
this.icon,
|
||||
this.link,
|
||||
this.getSourceTitle(),
|
||||
this.tags.tags
|
||||
this.tags
|
||||
)
|
@ -6,7 +6,7 @@ buildscript {
|
||||
}
|
||||
dependencies {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Wed Feb 09 17:05:19 CET 2022
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
@ -20,9 +20,12 @@ kotlin {
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-core:1.6.7")
|
||||
implementation("io.ktor:ktor-client-serialization:1.6.7")
|
||||
implementation("io.ktor:ktor-client-core:2.0.1")
|
||||
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("io.ktor:ktor-client-auth:2.0.1")
|
||||
implementation("org.jsoup:jsoup:1.14.3")
|
||||
}
|
||||
}
|
||||
@ -34,7 +37,7 @@ kotlin {
|
||||
}
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-android:1.6.7")
|
||||
implementation("io.ktor:ktor-client-android:2.0.1")
|
||||
}
|
||||
}
|
||||
val androidTest by getting {
|
||||
@ -60,7 +63,7 @@ kotlin {
|
||||
iosX64Test.dependsOn(this)
|
||||
iosArm64Test.dependsOn(this)
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-ios:1.6.7")
|
||||
implementation("io.ktor:ktor-client-ios:2.0.1")
|
||||
}
|
||||
//iosSimulatorArm64Test.dependsOn(this)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -1,188 +1,229 @@
|
||||
package bou.amine.apps.readerforselfossv2.rest
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.Html
|
||||
import bou.amine.apps.readerforselfossv2.rest.SelfossModel.SelfossModel.constructUrl
|
||||
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.features.*
|
||||
import io.ktor.client.features.json.*
|
||||
import io.ktor.client.features.json.serializer.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.*
|
||||
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.forms.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.jvm.JvmField
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class SelfossApi {
|
||||
/**
|
||||
* TODO:
|
||||
* Self signed certs
|
||||
* Timeout + 408
|
||||
* Auth digest/basic
|
||||
* Loging
|
||||
*/
|
||||
class SelfossApi(private val apiDetailsService: ApiDetailsService) {
|
||||
|
||||
val baseUrl = "http://10.0.2.2:8888"
|
||||
val userName = ""
|
||||
val password = ""
|
||||
val client = HttpClient() {
|
||||
install(JsonFeature) {
|
||||
serializer = KotlinxSerializer(kotlinx.serialization.json.Json {
|
||||
private val client = HttpClient() {
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
prettyPrint = true
|
||||
isLenient = 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) =
|
||||
"$baseUrl$path"
|
||||
"${apiDetailsService.getBaseUrl()}$path"
|
||||
|
||||
|
||||
suspend fun login() =
|
||||
client.get<String>(url("/login"))// Todo: params
|
||||
suspend fun login(): SelfossModel.SuccessResponse? =
|
||||
client.get(url("/login")) {
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
parameter("password", apiDetailsService.getPassword())
|
||||
}.body()
|
||||
|
||||
suspend fun getItems(type: String,
|
||||
items: Int,
|
||||
offset: Int,
|
||||
tag: String? = "",
|
||||
source: Long? = null,
|
||||
search: String? = "",
|
||||
updatedSince: String? = ""): List<SelfossModel.Item> =
|
||||
suspend fun getItems(
|
||||
type: String,
|
||||
items: Int,
|
||||
offset: Int,
|
||||
tag: String? = "",
|
||||
source: Long? = null,
|
||||
search: String? = "",
|
||||
updatedSince: String? = ""
|
||||
): List<SelfossModel.Item>? =
|
||||
client.get(url("/items")) {
|
||||
parameter("username", userName)
|
||||
parameter("password", password)
|
||||
parameter("type", type)
|
||||
parameter("tag", tag)
|
||||
parameter("source", source)
|
||||
parameter("search", search)
|
||||
parameter("updatedsince", updatedSince)
|
||||
parameter("items", items)
|
||||
parameter("offset", offset)
|
||||
}
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
parameter("password", apiDetailsService.getPassword())
|
||||
parameter("type", type)
|
||||
parameter("tag", tag)
|
||||
parameter("source", source)
|
||||
parameter("search", search)
|
||||
parameter("updatedsince", updatedSince)
|
||||
parameter("items", items)
|
||||
parameter("offset", offset)
|
||||
}.body()
|
||||
|
||||
suspend fun stats(): SelfossModel.Stats =
|
||||
suspend fun stats(): SelfossModel.Stats? =
|
||||
client.get(url("/stats")) {
|
||||
parameter("username", userName)
|
||||
parameter("password", password)
|
||||
}
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
parameter("password", apiDetailsService.getPassword())
|
||||
}.body()
|
||||
|
||||
suspend fun tags(): List<SelfossModel.Tag> =
|
||||
suspend fun tags(): List<SelfossModel.Tag>? =
|
||||
client.get(url("/tags")) {
|
||||
parameter("username", userName)
|
||||
parameter("password", password)
|
||||
}
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
parameter("password", apiDetailsService.getPassword())
|
||||
}.body()
|
||||
|
||||
suspend fun update(): String =
|
||||
suspend fun update(): SelfossModel.SuccessResponse? =
|
||||
client.get(url("/update")) {
|
||||
parameter("username", userName)
|
||||
parameter("password", password)
|
||||
}
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
parameter("password", apiDetailsService.getPassword())
|
||||
}.body()
|
||||
|
||||
suspend fun spouts(): Map<String, SelfossModel.Spout> =
|
||||
client.get(url("/sources/spouts")) {
|
||||
parameter("username", userName)
|
||||
parameter("password", password)
|
||||
}
|
||||
suspend fun spouts(): Map<String, SelfossModel.Spout>? =
|
||||
client.get(url("/a/spouts")) {
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
parameter("password", apiDetailsService.getPassword())
|
||||
}.body()
|
||||
|
||||
suspend fun sources(): List<SelfossModel.Source> =
|
||||
suspend fun sources(): ArrayList<SelfossModel.Source>? =
|
||||
client.get(url("/sources/list")) {
|
||||
parameter("username", userName)
|
||||
parameter("password", password)
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
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 =
|
||||
client.get(url("/api/about"))
|
||||
|
||||
suspend fun markAsRead(id: String): SelfossModel.SuccessResponse =
|
||||
private suspend fun createSource(
|
||||
title: String,
|
||||
url: String,
|
||||
spout: String,
|
||||
tags: String,
|
||||
filter: String
|
||||
): SelfossModel.SuccessResponse? =
|
||||
client.submitForm(
|
||||
url = url("/mark/$id"),
|
||||
formParameters = Parameters.build {
|
||||
append("username", userName)
|
||||
append("password", password)
|
||||
},
|
||||
encodeInQuery = true
|
||||
)
|
||||
url = url("/source"),
|
||||
formParameters = Parameters.build {
|
||||
append("username", apiDetailsService.getUserName())
|
||||
append("password", apiDetailsService.getPassword())
|
||||
append("title", title)
|
||||
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(
|
||||
url = url("/unmark/$id"),
|
||||
formParameters = Parameters.build {
|
||||
append("username", userName)
|
||||
append("password", password)
|
||||
},
|
||||
encodeInQuery = true
|
||||
)
|
||||
url = url("/source"),
|
||||
formParameters = Parameters.build {
|
||||
append("username", apiDetailsService.getUserName())
|
||||
append("password", apiDetailsService.getPassword())
|
||||
append("title", title)
|
||||
append("url", url)
|
||||
append("spout", spout)
|
||||
append("tags[]", tags)
|
||||
append("filter", filter)
|
||||
},
|
||||
encodeInQuery = true
|
||||
).body()
|
||||
|
||||
suspend fun starr(id: String): SelfossModel.SuccessResponse =
|
||||
client.submitForm(
|
||||
url = url("/starr/$id"),
|
||||
formParameters = Parameters.build {
|
||||
append("username", userName)
|
||||
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)
|
||||
}
|
||||
suspend fun deleteSource(id: String): SelfossModel.SuccessResponse? =
|
||||
client.delete(url("/source/$id")) {
|
||||
parameter("username", apiDetailsService.getUserName())
|
||||
parameter("password", apiDetailsService.getPassword())
|
||||
}.body()
|
||||
}
|
@ -1,15 +1,8 @@
|
||||
package bou.amine.apps.readerforselfossv2.rest
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.Html
|
||||
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 {
|
||||
|
||||
@ -61,19 +54,11 @@ class SelfossModel {
|
||||
data class Source(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val tags: String,
|
||||
val tags: List<String>,
|
||||
val spout: String,
|
||||
val error: String,
|
||||
val icon: String
|
||||
) {
|
||||
fun getIcon(baseUrl: String): String {
|
||||
return constructUrl(baseUrl, "favicons", icon)
|
||||
}
|
||||
|
||||
fun getTitleDecoded(): String {
|
||||
return Html.fromHtml(title).toString()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Item(
|
||||
@ -88,81 +73,5 @@ class SelfossModel {
|
||||
val link: String,
|
||||
val sourcetitle: 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("&url=")) {
|
||||
link.substringAfter("&url=")
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
} else {
|
||||
this.link.replace("&", "&")
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user