diff --git a/BREAKING.md b/BREAKING.md new file mode 100644 index 0000000..1b2a509 --- /dev/null +++ b/BREAKING.md @@ -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. \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..cfd5ad2 --- /dev/null +++ b/TODO.md @@ -0,0 +1,10 @@ +# TODO + + +- Service injection + +- Basic auth + * Self signed certs + * Timeout + 408 + +- Clean HTTP login \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/AddSourceActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/AddSourceActivity.kt index 2067b1c..f2b8161 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/AddSourceActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/AddSourceActivity.kt @@ -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 - api!!.spouts().enqueue(object : Callback> { - override fun onResponse( - call: Call>, - response: Response> - ) { - 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>, 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 { - override fun onResponse( - call: Call, - response: Response - ) { - 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, 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 { - override fun onResponse( - call: Call, - response: Response - ) { - 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, 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() } - }) + } } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt index 8178cf7..919430c 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/HomeActivity.kt @@ -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 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 = ArrayList() - private var allItems: ArrayList = ArrayList() + private var items: ArrayList = ArrayList() + private var allItems: ArrayList = 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 = emptyList() @@ -140,7 +151,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private lateinit var config: Config - data class DrawerData(val tags: List?, val sources: List?) + data class DrawerData(val tags: List?, val sources: List?) 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 { - override fun onFailure(call: Call, 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, response: Response) { - 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?) { + fun handleTags(maybeTags: List?) { 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?) { + fun handleHiddenTags(maybeTags: List?) { if (maybeTags == null) { if (loadedFromCache) { binding.mainDrawer.itemAdapter.add( @@ -589,7 +587,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { ) } } else { - val filteredHiddenTags: List = + val filteredHiddenTags: List = 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?) { + fun handleSources(maybeSources: List?) { 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? = null - var sources: List? + var tags: List? = null + var sources: List? fun sourcesApiCall() { - if (this@HomeActivity.isNetworkAccessible(null, offlineShortcut) && updateSources) { - api.sources.enqueue(object : Callback> { - override fun onResponse( - call: Call>?, - response: Response> - ) { - 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>?, 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> { - override fun onResponse( - call: Call>, - response: Response> - ) { - 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>?, 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 { - override fun onResponse( - call: Call, - response: Response - ) { + 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, 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) { + private fun updateItems(adapterItems: ArrayList) { items = adapterItems } @@ -1259,32 +1251,24 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } private fun handleOfflineActions() { - fun doAndReportOnFail(call: Call, action: ActionEntity) { - call.enqueue(object: Callback { - override fun onResponse( - call: Call, - response: Response - ) { - thread { - db.actionsDao().delete(action) - } - } - - override fun onFailure(call: Call, 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) } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt index 76c7542..a1eadc2 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/LoginActivity.kt @@ -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 { - 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, - response: Response - ) { - 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, t: Throwable) { - preferenceError(t) - } - }) + } } else { showProgress(false) } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt index 8bd1edd..d1a4129 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/ReaderActivity.kt @@ -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 = ArrayList() + var allItems: ArrayList = ArrayList() } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt index ca9d5da..bc45148 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/SourcesActivity.kt @@ -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 = ArrayList() + var items: ArrayList binding.recyclerView.setHasFixedSize(true) binding.recyclerView.layoutManager = mLayoutManager - if (this@SourcesActivity.isNetworkAccessible(binding.recyclerView)) { - api.sources.enqueue(object : Callback> { - override fun onResponse( - call: Call>, - response: Response> - ) { - if (response.body() != null && response.body()!!.isNotEmpty()) { - items = response.body() as ArrayList - } - 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>, t: Throwable) { + } else { Toast.makeText( this@SourcesActivity, R.string.cant_get_sources, Toast.LENGTH_SHORT ).show() } - }) + } } binding.fab.setOnClickListener { diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt index 9f20454..a10b8d8 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemCardAdapter.kt @@ -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, + override var items: ArrayList, 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) -> Unit + override val searchService: SearchService, + override val updateItems: (ArrayList) -> Unit ) : ItemsAdapter() { 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 ) } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt index de7a516..e4c52fd 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemListAdapter.kt @@ -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, + override var items: ArrayList, 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) -> Unit + override val searchService: SearchService, + override val updateItems: (ArrayList) -> Unit ) : ItemsAdapter() { 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 ) } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemsAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemsAdapter.kt index 79fb9c9..4f97d93 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemsAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/ItemsAdapter.kt @@ -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 : RecyclerView.Adapter() { - abstract var items: ArrayList + abstract var items: ArrayList 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) -> Unit + abstract val searchService: SearchService + abstract val updateItems: (ArrayList) -> 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 : 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 : 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 : 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) { + fun addItemsAtEnd(newItems: List) { val oldSize = items.size items.addAll(newItems) notifyItemRangeInserted(oldSize, newItems.size) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt index d885e82..b49168e 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/adapters/SourcesListAdapter.kt @@ -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, - private val api: SelfossApi + private val items: ArrayList, + private val api: SelfossApi, + private val apiDetailsService: ApiDetailsService ) : RecyclerView.Adapter() { 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 { - override fun onResponse( - call: Call, - response: Response - ) { - 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, 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() } - }) + } } } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/BooleanTypeAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/BooleanTypeAdapter.kt deleted file mode 100644 index 90d8a27..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/BooleanTypeAdapter.kt +++ /dev/null @@ -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 { - - @Throws(JsonParseException::class) - override fun deserialize( - json: JsonElement, - typeOfT: Type, - context: JsonDeserializationContext - ): Boolean? = - try { - json.asInt == 1 - } catch (e: Exception) { - json.asBoolean - } -} diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossApi.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossApi.kt deleted file mode 100644 index 1f59981..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossApi.kt +++ /dev/null @@ -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() - 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 = - service.loginToSelfoss(config.userLogin, config.userPassword) - - suspend fun readItems( - itemsNumber: Int, - offset: Int - ): retrofit2.Response> = - getItems("read", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) - - suspend fun newItems( - itemsNumber: Int, - offset: Int - ): retrofit2.Response> = - getItems("unread", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) - - suspend fun starredItems( - itemsNumber: Int, - offset: Int - ): retrofit2.Response> = - getItems("starred", SharedItems.tagFilter, SharedItems.sourceIDFilter, SharedItems.searchFilter, itemsNumber, offset) - - fun allItems(): Call> = - service.allItems(userName, password) - - suspend fun allNewItems(): retrofit2.Response> = - getItems("unread", null, null, null, 200, 0) - - suspend fun allReadItems(): retrofit2.Response> = - getItems("read", null, null, null, 200, 0) - - suspend fun allStarredItems(): retrofit2.Response> = - 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> = - service.getItems(type, tag, sourceId, search, null, userName, password, items, offset) - - suspend fun updateItems( - updatedSince: String - ): retrofit2.Response> = - service.getItems("read", null, null, null, updatedSince, userName, password, 200, 0) - - fun markItem(itemId: String): Call = - service.markAsRead(itemId, userName, password) - - fun unmarkItem(itemId: String): Call = - service.unmarkAsRead(itemId, userName, password) - - suspend fun readAll(ids: List): SuccessResponse = - service.markAllAsRead(ids, userName, password) - - fun starrItem(itemId: String): Call = - service.starr(itemId, userName, password) - - fun unstarrItem(itemId: String): Call = - service.unstarr(itemId, userName, password) - - suspend fun stats(): retrofit2.Response = service.stats(userName, password) - - val tags: Call> - get() = service.tags(userName, password) - - fun update(): Call = - service.update(userName, password) - - val apiVersion: Call - get() = service.version() - - val sources: Call> - get() = service.sources(userName, password) - - fun deleteSource(id: String): Call = - service.deleteSource(id, userName, password) - - fun spouts(): Call> = - service.spouts(userName, password) - - fun createSource( - title: String, - url: String, - spout: String, - tags: String, - filter: String - ): Call = - service.createSource(title, url, spout, tags, filter, userName, password) - - fun createSourceApi2( - title: String, - url: String, - spout: String, - tags: List, - filter: String - ): Call = - service.createSourceApi2(title, url, spout, tags, filter, userName, password) -} diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossFetching.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossFetching.kt deleted file mode 100644 index 6ecbf29..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossFetching.kt +++ /dev/null @@ -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) - 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>, db: AppDatabase, clearDatabase: Boolean) { - if (response.isSuccessful) { - if (clearDatabase) { - CoroutineScope(Dispatchers.IO).launch { - SharedItems.clearDBItems(db) - } - } - val allItems = response.body() as ArrayList - SharedItems.appendNewItems(allItems) - } -} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossModels.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossModels.kt deleted file mode 100644 index 455e0b9..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossModels.kt +++ /dev/null @@ -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 = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): Item = Item(source) - override fun newArray(size: Int): Array = 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(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 { - val allImages = ArrayList() - - 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 = - object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): SelfossTagType = - SelfossTagType(source) - - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - } - - constructor(source: Parcel) : this( - tags = source.readString().orEmpty() - ) - - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeString(tags) - } -} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossService.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossService.kt deleted file mode 100644 index 335c05e..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossService.kt +++ /dev/null @@ -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 - - @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> - - @GET("items") - fun allItems( - @Query("username") username: String, - @Query("password") password: String - ): Call> - - @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 - - @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 - - @FormUrlEncoded - @POST("mark") - suspend fun markAllAsRead( - @Field("ids[]") ids: List, - @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 - - @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 - - @GET("stats") - suspend fun stats( - @Query("username") username: String, - @Query("password") password: String - ): Response - - @GET("tags") - fun tags( - @Query("username") username: String, - @Query("password") password: String - ): Call> - - @GET("update") - fun update( - @Query("username") username: String, - @Query("password") password: String - ): Call - - @GET("sources/spouts") - fun spouts( - @Query("username") username: String, - @Query("password") password: String - ): Call> - - @GET("sources/list") - fun sources( - @Query("username") username: String, - @Query("password") password: String - ): Call> - - @GET("api/about") - fun version(): Call - - @DELETE("source/{id}") - fun deleteSource( - @Path("id") id: String, - @Query("username") username: String, - @Query("password") password: String - ): Call - - @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 - - @FormUrlEncoded - @POST("source") - fun createSourceApi2( - @Field("title") title: String, - @Field("url") url: String, - @Field("spout") spout: String, - @Field("tags[]") tags: List, - @Field("filter") filter: String, - @Query("username") username: String, - @Query("password") password: String - ): Call -} diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossTagTypeTypeAdapter.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossTagTypeTypeAdapter.kt deleted file mode 100644 index 318eeb0..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/api/selfoss/SelfossTagTypeTypeAdapter.kt +++ /dev/null @@ -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 { - - @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()) - } -} diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt index fd930a8..47aa14f 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/background/background.kt @@ -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() // 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 doAndReportOnFail(call: Call, action: ActionEntity) { - call.enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { - 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: Throwable) { - } - }) + } } } \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt index cd39850..97b30ea 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/fragments/ArticleFragment.kt @@ -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 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 } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt new file mode 100644 index 0000000..bff553f --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/AndroidIModelUtils.kt @@ -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 { + val allImages = ArrayList() + + 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() + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/ParecelableItem.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/ParecelableItem.kt new file mode 100644 index 0000000..1de2e17 --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/model/ParecelableItem.kt @@ -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 = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): ParecelableItem = ParecelableItem(source) + override fun newArray(size: Int): Array = 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) + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/AndroidDeviceDatabase.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/AndroidDeviceDatabase.kt new file mode 100644 index 0000000..1b9ca40 --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/AndroidDeviceDatabase.kt @@ -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 { + 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 = 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) +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/AndroidDeviceDatabaseService.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/AndroidDeviceDatabaseService.kt new file mode 100644 index 0000000..f7178ff --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/AndroidDeviceDatabaseService.kt @@ -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(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) { + var tmpItems = items + if (tmpItems != newItems) { + tmpItems = tmpItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList + tmpItems.addAll(newItems) + items = tmpItems + + sortItems() + getFocusedItems() + } + } + + override fun getFromDB() { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/dao/ItemsDao.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/dao/ItemsDao.kt index d85eb66..d12ec74 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/dao/ItemsDao.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/dao/ItemsDao.kt @@ -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 + suspend fun items(): List @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) } \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/database/AppDatabase.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/database/AppDatabase.kt index 20be67a..b9c0bdc 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/database/AppDatabase.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/database/AppDatabase.kt @@ -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 diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/entities/ItemEntity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/entities/AndroidItemEntity.kt similarity index 90% rename from androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/entities/ItemEntity.kt rename to androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/entities/AndroidItemEntity.kt index 5b97779..4888bdc 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/entities/ItemEntity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/persistence/entities/AndroidItemEntity.kt @@ -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, diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/service/AndroidApiDetailsService.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/service/AndroidApiDetailsService.kt new file mode 100644 index 0000000..96cdce3 --- /dev/null +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/service/AndroidApiDetailsService.kt @@ -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 + } +} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/ApiUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/ApiUtils.kt deleted file mode 100644 index 51a9458..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/ApiUtils.kt +++ /dev/null @@ -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.succeeded(): Boolean = - this.code() === 200 && this.body() != null && this.body()!!.isSuccess \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/AppUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/AppUtils.kt index 045b1d7..6fd2818 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/AppUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/AppUtils.kt @@ -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() diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/DateUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/DateUtils.kt deleted file mode 100644 index a6419d2..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/DateUtils.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/ItemsUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/ItemsUtils.kt index 3bae704..e713c37 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/ItemsUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/ItemsUtils.kt @@ -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.flattenTags(): List = +fun List.flattenTags(): List = this.flatMap { val item = it - val tags: List = it.tags.tags.split(",") + val tags: List = it.tags.split(",") tags.map { t -> - item.copy(tags = SelfossTagType(t.trim())) + item.copy(tags = t.trim()) } } \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/LinksUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/LinksUtils.kt index 01cf692..b642c70 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/LinksUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/LinksUtils.kt @@ -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, + allItems: ArrayList, 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, + allItems: ArrayList, 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()) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/SharedItems.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/SharedItems.kt deleted file mode 100644 index c8e7dda..0000000 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/SharedItems.kt +++ /dev/null @@ -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 = arrayListOf() - get() { - return ArrayList(field) - } - set(value) { - field = ArrayList(value) - } - var focusedItems: ArrayList = arrayListOf() - 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) { - var tmpItems = items - if (tmpItems != newItems) { - tmpItems = tmpItems.filter { item -> newItems.find { it.id == item.id } == null } as ArrayList - tmpItems.addAll(newItems) - items = tmpItems - - sortItems() - getFocusedItems() - } - } - - fun refreshFocusedItems(newItems: ArrayList) { - 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 - } - if (searchFilter != null) { - tmpItems = tmpItems.filter { filterSearch(it) } as ArrayList - } - if (sourceFilter != null) { - tmpItems = tmpItems.filter { it.sourcetitle == sourceFilter } as ArrayList - } - 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 - filter() - } - - fun getRead() { - displayedItems = "read" - focusedItems = items.filter { item -> !item.unread } as ArrayList - filter() - } - - fun getStarred() { - displayedItems = "starred" - focusedItems = items.filter { item -> item.starred } as ArrayList - 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 - 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) { - 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 { - override fun onResponse( - call: Call, - response: Response - ) { - - val tmpItems = items - tmpItems[position].unread = false - items = tmpItems - - resetDBItem(db) - getFocusedItems() - badgeUnread-- - } - - override fun onFailure(call: Call, 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 { - override fun onResponse( - call: Call, - response: Response - ) { - - val tmpItems = items - tmpItems[position].unread = true - items = tmpItems - - resetDBItem(db) - getFocusedItems() - badgeUnread++ - } - - override fun onFailure(call: Call, 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 { - override fun onResponse( - call: Call, - response: Response - ) { - val tmpItems = items - tmpItems[position].starred = true - items = tmpItems - - resetDBItem(db) - getFocusedItems() - badgeStarred++ - } - - override fun onFailure( - call: Call, - 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 { - override fun onResponse( - call: Call, - response: Response - ) { - val tmpItems = items - tmpItems[position].starred = false - items = tmpItems - - resetDBItem(db) - getFocusedItems() - badgeStarred-- - } - - override fun onFailure( - call: Call, - 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 - } -} \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt index 9f7cf2e..2edfc1c 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/network/NetworkUtils.kt @@ -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 } } \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/persistence/EntitiesUtils.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/persistence/EntitiesUtils.kt index 4faaf44..6f85025 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/persistence/EntitiesUtils.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/persistence/EntitiesUtils.kt @@ -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 ) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index eb35706..012c4be 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a84eecc..e4ddf24 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 954690b..f09de7c 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -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) } diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/dao/DeviceDatabase.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/dao/DeviceDatabase.kt new file mode 100644 index 0000000..fd81b6c --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/dao/DeviceDatabase.kt @@ -0,0 +1,9 @@ +package bou.amine.apps.readerforselfossv2.dao + +interface DeviceDatabase { + suspend fun items(): List + suspend fun insertAllItems(vararg items: ItemEntity) + suspend fun deleteAllItems() + suspend fun delete(item: ItemEntity) + suspend fun updateItem(item: ItemEntity) +} diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt index 6fd178e..ac43883 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossApi.kt @@ -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(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 = + suspend fun getItems( + type: String, + items: Int, + offset: Int, + tag: String? = "", + source: Long? = null, + search: String? = "", + updatedSince: String? = "" + ): List? = 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 = + suspend fun tags(): List? = 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 = - client.get(url("/sources/spouts")) { - parameter("username", userName) - parameter("password", password) - } + suspend fun spouts(): Map? = + client.get(url("/a/spouts")) { + parameter("username", apiDetailsService.getUserName()) + parameter("password", apiDetailsService.getPassword()) + }.body() - suspend fun sources(): List = + suspend fun sources(): ArrayList? = 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): 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): 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(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() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossModel.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossModel.kt index dbb2896..a9c2d47 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossModel.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/rest/SelfossModel.kt @@ -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, 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 { - val allImages = ArrayList() - - 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() - } - } - } + ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsService.kt new file mode 100644 index 0000000..77214c9 --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsService.kt @@ -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 +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/DeviceDataBaseService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/DeviceDataBaseService.kt new file mode 100644 index 0000000..bd14d22 --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/DeviceDataBaseService.kt @@ -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(val db: DeviceDatabase, private val searchService: SearchService) { + var itemsCaching = false + var items: ArrayList = arrayListOf() + get() { + return ArrayList(field) + } + set(value) { + field = ArrayList(value) + } + + abstract suspend fun updateDatabase() + abstract suspend fun clearDBItems() + abstract fun appendNewItems(items: List) + 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 + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/SearchService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/SearchService.kt new file mode 100644 index 0000000..e929eba --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/SearchService.kt @@ -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 +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/SelfossService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/SelfossService.kt new file mode 100644 index 0000000..ddc9b85 --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/SelfossService.kt @@ -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(val api: SelfossApi, private val dbService: DeviceDataBaseService, 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) + 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?, clearDatabase: Boolean) { + if (response != null) { + if (clearDatabase) { + CoroutineScope(Dispatchers.Default).launch { + dbService.clearDBItems() + } + } + dbService.appendNewItems(response) + } + } + + private suspend fun allNewItems(): List? = + readItems(200, 0) + + private suspend fun allReadItems(): List? = + newItems(200, 0) + + private suspend fun allStarredItems(): List? = + starredItems(200, 0) + + private suspend fun readItems( + itemsNumber: Int, + offset: Int + ): List? = + api.getItems("read", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter) + + private suspend fun newItems( + itemsNumber: Int, + offset: Int + ): List? = + api.getItems("unread", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter) + + private suspend fun starredItems( + itemsNumber: Int, + offset: Int + ): List? = + api.getItems("starred", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt new file mode 100644 index 0000000..2bafad3 --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/StringUtils.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/StringUtils.kt new file mode 100644 index 0000000..aa0cebf --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/StringUtils.kt @@ -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 + } \ No newline at end of file