diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index f675617..d4a17f7 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -170,6 +170,13 @@ dependencies { implementation("me.relex:circleindicator:2.1.6") implementation("androidx.viewpager2:viewpager2:1.1.0-beta01") + //Dependency Injection + implementation("org.kodein.di:kodein-di:7.12.0") + implementation("org.kodein.di:kodein-di-framework-android-x:7.12.0") + + //Settings + implementation("com.russhwolf:multiplatform-settings-no-arg:0.9") + //PhotoView implementation("com.github.chrisbanes:PhotoView:2.3.0") 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 681b285..0edd1c2 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 @@ -1,47 +1,36 @@ package bou.amine.apps.readerforselfossv2.android -import android.content.Context import android.content.Intent import android.os.Bundle -import androidx.preference.PreferenceManager -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.appcompat.app.AppCompatActivity import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.EditText -import android.widget.ProgressBar -import android.widget.Spinner -import android.widget.TextView -import android.widget.Toast +import android.widget.* +import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding 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.isBaseUrlValid +import bou.amine.apps.readerforselfossv2.repository.Repository import com.ftinc.scoop.Scoop -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 +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.instance -class AddSourceActivity : AppCompatActivity() { +class AddSourceActivity : AppCompatActivity(), DIAware { - private lateinit var apiDetailsService: ApiDetailsService private var mSpoutsValue: String? = null - private lateinit var api: SelfossApi private lateinit var appColors: AppColors private lateinit var binding: ActivityAddSourceBinding + override val di by closestDI() + private val repository : Repository by instance() + override fun onCreate(savedInstanceState: Bundle?) { appColors = AppColors(this@AddSourceActivity) @@ -76,45 +65,32 @@ class AddSourceActivity : AppCompatActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) - try { - 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() - apiDetailsService - ) - } catch (e: IllegalArgumentException) { - mustLoginToAddSource() - } - maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput) binding.saveBtn.setTextColor(appColors.colorAccent) binding.saveBtn.setOnClickListener { - handleSaveSource(binding.tags, binding.nameInput.text.toString(), binding.sourceUri.text.toString(), api) + handleSaveSource( + binding.tags, + binding.nameInput.text.toString(), + binding.sourceUri.text.toString() + ) } } override fun onResume() { super.onResume() - val config = Config(this) + val config = Config() if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) { mustLoginToAddSource() } else { - handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer) + handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer) } } private fun handleSpoutsSpinner( spoutsSpinner: Spinner, - api: SelfossApi?, mProgress: ProgressBar, formContainer: ConstraintLayout ) { @@ -134,7 +110,7 @@ class AddSourceActivity : AppCompatActivity() { CoroutineScope(Dispatchers.Main).launch { - var items = api!!.spouts() + val items = repository.getSpouts() if (items != null) { val itemsStrings = items.map { it.value.name } @@ -182,7 +158,7 @@ class AddSourceActivity : AppCompatActivity() { finish() } - private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) { + private fun handleSaveSource(tags: EditText, title: String, url: String) { val sourceDetailsUnavailable = title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() @@ -193,15 +169,14 @@ class AddSourceActivity : AppCompatActivity() { } else -> { CoroutineScope(Dispatchers.Main).launch { - val response: SelfossModel.SuccessResponse? = api.createSourceForVersion( - title, - url, - mSpoutsValue!!, - tags.text.toString(), - "", - PreferenceManager.getDefaultSharedPreferences(this@AddSourceActivity).getInt("apiVersionMajor", 0) - ) - if (response != null) { + val successfullyAddedSource = repository.createSource( + title, + url, + mSpoutsValue!!, + tags.text.toString(), + "", + ) + if (successfullyAddedSource) { finish() } else { Toast.makeText( 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 81fd1b9..2d7e628 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 @@ -1,14 +1,11 @@ package bou.amine.apps.readerforselfossv2.android -import android.content.Context import android.content.Intent -import android.content.SharedPreferences import android.graphics.Color import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.net.Uri import android.os.Bundle -import androidx.preference.PreferenceManager import android.view.Menu import android.view.MenuItem import android.view.View @@ -37,11 +34,9 @@ import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDataba 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 @@ -52,13 +47,9 @@ import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActiv 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.utils.DateUtils -import bou.amine.apps.readerforselfossv2.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository 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.ItemType import bou.amine.apps.readerforselfossv2.utils.longHash import com.ashokvarma.bottomnavigation.BottomNavigationBar import com.ashokvarma.bottomnavigation.BottomNavigationItem @@ -80,30 +71,27 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.util.addStickyFooterItem import com.mikepenz.materialdrawer.util.updateBadge import com.mikepenz.materialdrawer.widget.AccountHeaderView +import com.russhwolf.settings.Settings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.instance import java.util.concurrent.TimeUnit import kotlin.concurrent.thread -class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { +class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware { 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 private val DRAWER_ID_SOURCES = 100110L private val DRAWER_ID_FILTERS = 100111L - private val UNREAD_SHOWN = 1 - private val READ_SHOWN = 2 - private val FAV_SHOWN = 3 private var items: ArrayList = ArrayList() - private var allItems: ArrayList = ArrayList() private var internalBrowser = false private var articleViewer = false @@ -112,15 +100,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private var displayAllCount = false private var fullHeightCards: Boolean = false private var itemsNumber: Int = 200 - private var elementsShown: Int = 1 - private var userIdentifier: String = "" + private var elementsShown: ItemType = ItemType.UNREAD private var displayAccountHeader: Boolean = false private var infiniteScroll: Boolean = false private var lastFetchDone: Boolean = false private var updateSources: Boolean = true private var markOnScroll: Boolean = false private var hiddenTags: List = emptyList() - private var apiVersionMajor: Int = 0 private var periodicRefresh = false private var refreshMinutes: Long = 360L @@ -129,15 +115,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private lateinit var tabNewBadge: TextBadgeItem private lateinit var tabArchiveBadge: TextBadgeItem private lateinit var tabStarredBadge: TextBadgeItem - private lateinit var api: SelfossApi private lateinit var customTabActivityHelper: CustomTabActivityHelper - private lateinit var editor: SharedPreferences.Editor - private lateinit var sharedPref: SharedPreferences private lateinit var appColors: AppColors private var offset: Int = 0 private var firstVisible: Int = 0 private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener - private lateinit var settings: SharedPreferences + private var settings = Settings() private lateinit var binding: ActivityHomeBinding private var recyclerAdapter: RecyclerView.Adapter<*>? = null @@ -151,6 +134,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private lateinit var config: Config + override val di by closestDI() + private val repository : Repository by instance() + data class DrawerData(val tags: List?, val sources: List?) override fun onStart() { @@ -160,7 +146,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { override fun onCreate(savedInstanceState: Bundle?) { appColors = AppColors(this@HomeActivity) - config = Config(this@HomeActivity) + config = Config() super.onCreate(savedInstanceState) binding = ActivityHomeBinding.inflate(layoutInflater) @@ -170,7 +156,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { offlineShortcut = intent.getBooleanExtra("startOffline", false) if (fromTabShortcut) { - elementsShown = intent.getIntExtra("shortcutTab", UNREAD_SHOWN) + elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position)) } setContentView(view) @@ -192,33 +178,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { customTabActivityHelper = CustomTabActivityHelper() - 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() - apiDetailsService - ) - dataBase = AndroidDeviceDatabase(applicationContext) - searchService = SearchService(DateUtils(apiDetailsService)) - dbService = AndroidDeviceDatabaseService(dataBase, searchService) - service = SelfossService(api, dbService, searchService) - items = ArrayList() - allItems = ArrayList() handleBottomBar() handleDrawer() handleSwipeRefreshLayout() - handleSharedPrefs() - - getApiMajorVersion() + handleSettings() getElementsAccordingToTab() } @@ -231,7 +198,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { ) binding.swipeRefreshLayout.setOnRefreshListener { offlineShortcut = false - allItems = ArrayList() lastFetchDone = false handleDrawerItems() CoroutineScope(Dispatchers.Main).launch { @@ -249,7 +215,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ): Int = - if (elementsShown == FAV_SHOWN) { + if (elementsShown == ItemType.STARRED) { 0 } else { super.getSwipeDirs( @@ -346,17 +312,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { binding.bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC) if (fromTabShortcut) { - binding.bottomBar.selectTab(elementsShown - 1) - } - } - - private fun getApiMajorVersion() { - CoroutineScope(Dispatchers.IO).launch { - val version = api.version() - if (version != null) { - apiVersionMajor = version.getApiMajorVersion() - sharedPref.edit().putInt("apiVersionMajor", apiVersionMajor).apply() - } + binding.bottomBar.selectTab(elementsShown.position - 1) } } @@ -366,10 +322,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { // TODO: Make this the only appcolors init appColors = AppColors(this@HomeActivity) - sharedPref = PreferenceManager.getDefaultSharedPreferences(this) - - editor = settings.edit() - handleDrawerItems() handleThemeUpdate() @@ -396,34 +348,30 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { customTabActivityHelper.unbindCustomTabsService(this) } - private fun handleSharedPrefs() { - internalBrowser = sharedPref.getBoolean("prefer_internal_browser", true) - articleViewer = sharedPref.getBoolean("prefer_article_viewer", true) - shouldBeCardView = sharedPref.getBoolean("card_view_active", false) - displayUnreadCount = sharedPref.getBoolean("display_unread_count", true) - displayAllCount = sharedPref.getBoolean("display_other_count", false) - fullHeightCards = sharedPref.getBoolean("full_height_cards", false) - itemsNumber = sharedPref.getString("prefer_api_items_number", "200")!!.toInt() - userIdentifier = sharedPref.getString("unique_id", "")!! - displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false) - infiniteScroll = sharedPref.getBoolean("infinite_loading", false) - 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()) { - sharedPref.getString("hidden_tags", "")!!.replace("\\s".toRegex(), "").split(",") + private fun handleSettings() { + internalBrowser = settings.getBoolean("prefer_internal_browser", true) + articleViewer = settings.getBoolean("prefer_article_viewer", true) + shouldBeCardView = settings.getBoolean("card_view_active", false) + displayUnreadCount = settings.getBoolean("display_unread_count", true) + displayAllCount = settings.getBoolean("display_other_count", false) + fullHeightCards = settings.getBoolean("full_height_cards", false) + itemsNumber = settings.getString("prefer_api_items_number", "200").toInt() + displayAccountHeader = settings.getBoolean("account_header_displaying", false) + infiniteScroll = settings.getBoolean("infinite_loading", false) + updateSources = settings.getBoolean("update_sources", true) + markOnScroll = settings.getBoolean("mark_on_scroll", false) + hiddenTags = if (settings.getString("hidden_tags", "").isNotEmpty()) { + settings.getString("hidden_tags", "").replace("\\s".toRegex(), "").split(",") } else { emptyList() } - periodicRefresh = sharedPref.getBoolean("periodic_refresh", false) - refreshWhenChargingOnly = sharedPref.getBoolean("refresh_when_charging", false) - refreshMinutes = sharedPref.getString("periodic_refresh_minutes", "360")!!.toLong() + periodicRefresh = settings.getBoolean("periodic_refresh", false) + refreshWhenChargingOnly = settings.getBoolean("refresh_when_charging", false) + refreshMinutes = settings.getString("periodic_refresh_minutes", "360").toLong() if (refreshMinutes <= 15) { refreshMinutes = 15 } - - apiVersionMajor = sharedPref.getInt("apiVersionMajor", 0) } private fun handleThemeBinding() { @@ -478,8 +426,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { binding.drawerContainer.addDrawerListener(drawerListener) displayAccountHeader = - PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean("account_header_displaying", false) + settings.getBoolean("account_header_displaying", false) binding.mainDrawer.addStickyFooterItem( PrimaryDrawerItem().apply { @@ -509,7 +456,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { attachToSliderView(binding.mainDrawer) addProfiles( ProfileDrawerItem().apply { - nameText = settings.getString("url", "").toString() + nameText = settings.getString("url", "") setBackgroundResource(R.drawable.bg) iconRes = R.mipmap.ic_launcher selectionListEnabledForSingleProfile = false @@ -556,10 +503,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { textColor = ColorHolder.fromColor(Color.WHITE) color = ColorHolder.fromColor(appColors.colorAccent) } onDrawerItemClickListener = { _,_,_ -> - allItems = ArrayList() - searchService.tagFilter = it.tag - searchService.sourceFilter = null - searchService.sourceIDFilter = null + repository.tagFilter = it + repository.sourceFilter = null getElementsAccordingToTab() fetchOnEmptyList() false @@ -609,10 +554,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { textColor = ColorHolder.fromColor(Color.WHITE) color = ColorHolder.fromColor(appColors.colorAccent) } onDrawerItemClickListener = { _,_,_ -> - allItems = ArrayList() - searchService.tagFilter = it.tag - searchService.sourceFilter = null - searchService.sourceIDFilter = null + repository.tagFilter = it + repository.sourceFilter = null getElementsAccordingToTab() fetchOnEmptyList() false @@ -643,12 +586,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { val item = PrimaryDrawerItem().apply { nameText = source.getTitleDecoded() identifier = source.id.toLong() - iconUrl = source.getIcon(apiDetailsService.getBaseUrl()) + iconUrl = source.getIcon(repository.baseUrl) onDrawerItemClickListener = { _,_,_ -> - allItems = ArrayList() - searchService.sourceIDFilter = source.id.toLong() - searchService.sourceFilter = source.title - searchService.tagFilter = null + repository.sourceFilter = source + repository.tagFilter = null getElementsAccordingToTab() fetchOnEmptyList() false @@ -668,10 +609,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { identifier = DRAWER_ID_FILTERS badgeRes = R.string.drawer_action_clear onDrawerItemClickListener = { _,_,_ -> - allItems = ArrayList() - searchService.sourceFilter = null - searchService.sourceIDFilter = null - searchService.tagFilter = null + repository.sourceFilter = null + repository.tagFilter = null binding.mainDrawer.setSelectionAtPosition(-1) getElementsAccordingToTab() fetchOnEmptyList() @@ -772,7 +711,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { fun sourcesApiCall() { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) { CoroutineScope(Dispatchers.Main).launch { - val response = api.sources() + val response = repository.getSources() if (response != null) { sources = response val apiDrawerData = DrawerData(tags, sources) @@ -791,10 +730,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) { CoroutineScope(Dispatchers.IO).launch { - val response = api.tags() - if (response != null) { - tags = response - } + tags = repository.getTags() sourcesApiCall() } } @@ -889,7 +825,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { offset = 0 lastFetchDone = false - elementsShown = position + 1 + elementsShown = ItemType.fromInt(position + 1) getElementsAccordingToTab() binding.recyclerView.scrollToPosition(0) @@ -942,15 +878,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private fun getElementsAccordingToTab( appendResults: Boolean = false ) { - fun doGetAccordingToTab() { - when (elementsShown) { - UNREAD_SHOWN -> getUnRead(appendResults) - READ_SHOWN -> getRead(appendResults) - FAV_SHOWN -> getStarred(appendResults) - else -> getUnRead(appendResults) - } - } - offset = if (appendResults && items.size > 0) { items.size - 1 } else { @@ -958,45 +885,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } firstVisible = if (appendResults) firstVisible else 0 - doGetAccordingToTab() + getItems(appendResults, elementsShown) } - private fun getUnRead(appendResults: Boolean = false) { + private fun getItems(appendResults: Boolean, itemType: ItemType) { CoroutineScope(Dispatchers.Main).launch { binding.swipeRefreshLayout.isRefreshing = true - val apiItems = service.getUnreadItems(itemsNumber, offset, applicationContext.isNetworkAvailable()) - if (appendResults) { - apiItems?.let { items.addAll(it) } + repository.displayedItems = itemType + items = if (appendResults) { + repository.getOlderItems() } else { - items = apiItems.orEmpty() as ArrayList - } - binding.swipeRefreshLayout.isRefreshing = false - handleListResult() - } - } - - private fun getRead(appendResults: Boolean = false) { - CoroutineScope(Dispatchers.Main).launch { - binding.swipeRefreshLayout.isRefreshing = true - val apiItems = service.getReadItems(itemsNumber, offset, applicationContext.isNetworkAvailable()) - if (appendResults) { - apiItems?.let { items.addAll(it) } - } else { - items = apiItems.orEmpty() as ArrayList - } - binding.swipeRefreshLayout.isRefreshing = false - handleListResult() - } - } - - private fun getStarred(appendResults: Boolean = false) { - CoroutineScope(Dispatchers.Main).launch { - binding.swipeRefreshLayout.isRefreshing = true - val apiItems = service.getStarredItems(itemsNumber, offset, applicationContext.isNetworkAvailable()) - if (appendResults) { - apiItems?.let { items.addAll(it) } - } else { - items = apiItems.orEmpty() as ArrayList + repository.getNewerItems() } binding.swipeRefreshLayout.isRefreshing = false handleListResult() @@ -1021,17 +920,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { ItemCardAdapter( this, items, - api, - apiDetailsService, db, customTabActivityHelper, internalBrowser, articleViewer, fullHeightCards, appColors, - userIdentifier, - config, - searchService + config ) { updateItems(it) } @@ -1040,16 +935,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { ItemListAdapter( this, items, - api, - apiDetailsService, db, customTabActivityHelper, internalBrowser, articleViewer, - userIdentifier, appColors, - config, - searchService + config ) { updateItems(it) } @@ -1073,8 +964,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private fun reloadBadges() { if (displayUnreadCount || displayAllCount) { CoroutineScope(Dispatchers.Main).launch { - service.reloadBadges(applicationContext.isNetworkAvailable()) - reloadBadgeContent() + if (applicationContext.isNetworkAvailable()) { + repository.reloadBadges() + reloadBadgeContent() + } } } } @@ -1082,15 +975,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private fun reloadBadgeContent() { if (displayUnreadCount) { tabNewBadge - .setText(searchService.badgeUnread.toString()) + .setText(repository.badgeUnread.toString()) .maybeShow() } if (displayAllCount) { tabArchiveBadge - .setText(searchService.badgeAll.toString()) + .setText(repository.badgeAll.toString()) .maybeShow() tabStarredBadge - .setText(searchService.badgeStarred.toString()) + .setText(repository.badgeStarred.toString()) .maybeShow() } } @@ -1110,7 +1003,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { override fun onQueryTextChange(p0: String?): Boolean { if (p0.isNullOrBlank()) { - searchService.searchFilter = null + repository.searchFilter = null getElementsAccordingToTab() fetchOnEmptyList() } @@ -1118,7 +1011,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } override fun onQueryTextSubmit(p0: String?): Boolean { - searchService.searchFilter = p0 + repository.searchFilter = p0 getElementsAccordingToTab() fetchOnEmptyList() return false @@ -1151,9 +1044,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() + // TODO: Use Dispatchers.IO CoroutineScope(Dispatchers.Main).launch { - val status = api.update() - if (status != null && status.isSuccess) { + val updatedRemote = repository.updateRemote() + if (updatedRemote) { Toast.makeText( this@HomeActivity, R.string.refresh_success_response, Toast.LENGTH_LONG @@ -1174,13 +1068,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } } R.id.readAll -> { - if (elementsShown == UNREAD_SHOWN) { + if (elementsShown == ItemType.UNREAD) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { binding.swipeRefreshLayout.isRefreshing = true if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) { CoroutineScope(Dispatchers.Main).launch { - val success = service.readAll(items.map { it.id.toString() }, applicationContext.isNetworkAvailable()) + val success = repository.markAllAsRead(items.map { it.id }) if (success) { Toast.makeText( this@HomeActivity, @@ -1208,7 +1102,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { return true } R.id.action_disconnect -> { - return Config.logoutAndRedirect(this, this@HomeActivity, editor) + return Config.logoutAndRedirect(this, this@HomeActivity) } else -> return super.onOptionsItemSelected(item) } @@ -1216,10 +1110,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { private fun maxItemNumber(): Int = when (elementsShown) { - UNREAD_SHOWN -> searchService.badgeUnread - READ_SHOWN -> searchService.badgeAll - FAV_SHOWN -> searchService.badgeStarred - else -> searchService.badgeUnread // if !elementsShown then unread are fetched. + ItemType.UNREAD -> repository.badgeUnread + ItemType.ALL -> repository.badgeAll + ItemType.STARRED -> repository.badgeStarred + else -> repository.badgeUnread // if !elementsShown then unread are fetched. } private fun updateItems(adapterItems: ArrayList) { @@ -1245,8 +1139,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { } private fun handleOfflineActions() { - fun doAndReportOnFail(call: SelfossModel.SuccessResponse?, action: ActionEntity) { - if (call != null && call.isSuccess) { + fun doAndReportOnFail(success: Boolean, action: ActionEntity) { + if (success) { thread { db.actionsDao().delete(action) } @@ -1259,10 +1153,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { actions.forEach { action -> when { - 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) + action.read -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action) + action.unread -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action) + action.starred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action) + action.unstarred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), 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 a1eadc2..3ef0544 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 @@ -3,48 +3,43 @@ package bou.amine.apps.readerforselfossv2.android import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.content.Intent -import android.content.SharedPreferences 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 androidx.work.Logger +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity 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.isNetworkAvailable -import bou.amine.apps.readerforselfossv2.rest.SelfossApi -import bou.amine.apps.readerforselfossv2.service.ApiDetailsService +import bou.amine.apps.readerforselfossv2.repository.Repository import com.mikepenz.aboutlibraries.LibsBuilder +import com.russhwolf.settings.Settings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.instance -class LoginActivity : AppCompatActivity() { +class LoginActivity() : AppCompatActivity(), DIAware { private var inValidCount: Int = 0 private var isWithSelfSignedCert = false private var isWithLogin = false private var isWithHTTPLogin = false - private lateinit var settings: SharedPreferences - private lateinit var editor: SharedPreferences.Editor - private lateinit var userIdentifier: String + private val settings = Settings() private lateinit var appColors: AppColors private lateinit var binding: ActivityLoginBinding + override val di by closestDI() + private val repository : Repository by instance() + override fun onCreate(savedInstanceState: Bundle?) { appColors = AppColors(this@LoginActivity) @@ -58,12 +53,7 @@ class LoginActivity : AppCompatActivity() { handleBaseUrlFail() - settings = PreferenceManager.getDefaultSharedPreferences(applicationContext) - userIdentifier = settings.getString("unique_id", "")!! - - editor = settings.edit() - - if (settings.getString("url", "")!!.isNotEmpty()) { + if (settings.getString("url", "").isNotEmpty()) { goToMain() } @@ -128,18 +118,16 @@ class LoginActivity : AppCompatActivity() { } private fun preferenceError(t: Throwable) { - editor.remove("url") - editor.remove("login") - editor.remove("httpUserName") - editor.remove("password") - editor.remove("httpPassword") - editor.apply() + settings.remove("url") + settings.remove("login") + settings.remove("httpUserName") + settings.remove("password") + settings.remove("httpPassword") 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() { @@ -212,39 +200,21 @@ class LoginActivity : AppCompatActivity() { } else { showProgress(true) - editor.putString("url", url) - editor.putString("login", login) - editor.putString("httpUserName", httpLogin) - editor.putString("password", password) - editor.putString("httpPassword", httpPassword) - editor.putBoolean("isSelfSignedCert", isWithSelfSignedCert) - editor.apply() - - val apiDetailsService = AndroidApiDetailsService(this@LoginActivity) - val api = SelfossApi( -// this, -// this@LoginActivity, -// isWithSelfSignedCert, -// -1L - apiDetailsService - ) + repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert) 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 { + val result = repository.login() + if (result) { + goToMain() + } else { + CoroutineScope(Dispatchers.Main).launch { preferenceError(Exception("Not success")) } - } catch (cause: Throwable) { - Log.e("1", "LOL") } } - } else { - showProgress(false) } + showProgress(false) } } diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt index 7d19deb..d0e190e 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/MyApp.kt @@ -6,31 +6,35 @@ import android.content.Context import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build -import androidx.preference.PreferenceManager import android.widget.ImageView import androidx.multidex.MultiDexApplication +import androidx.preference.PreferenceManager +import bou.amine.apps.readerforselfossv2.DI.networkModule import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth +import bou.amine.apps.readerforselfossv2.repository.Repository import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.ftinc.scoop.Scoop import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader -import java.util.UUID.randomUUID +import com.russhwolf.settings.Settings +import org.kodein.di.* + +class MyApp : MultiDexApplication(), DIAware { + + override val di by DI.lazy { + import(networkModule) + bind() with singleton { Repository(instance(), instance()) } + } -class MyApp : MultiDexApplication() { private lateinit var config: Config + private lateinit var settings : Settings override fun onCreate() { super.onCreate() - config = Config(baseContext) - - val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) - if (prefs.getString("unique_id", "")!!.isEmpty()) { - val editor = prefs.edit() - editor.putString("unique_id", randomUUID().toString()) - editor.apply() - } + config = Config() + settings = Settings() initDrawerImageLoader() 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 7a945bd..f4eb03d 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 @@ -1,7 +1,5 @@ package bou.amine.apps.readerforselfossv2.android -import android.content.Context -import android.content.SharedPreferences import android.graphics.Color import android.os.Bundle import android.view.KeyEvent @@ -10,7 +8,6 @@ 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 @@ -20,38 +17,38 @@ import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabas 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.toggleStar -import bou.amine.apps.readerforselfossv2.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.rest.SelfossModel -import bou.amine.apps.readerforselfossv2.service.ApiDetailsService import com.ftinc.scoop.Scoop +import com.russhwolf.settings.Settings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.instance -class ReaderActivity : AppCompatActivity() { +class ReaderActivity : AppCompatActivity(), DIAware { private var markOnScroll: Boolean = false private var currentItem: Int = 0 - private lateinit var userIdentifier: String private lateinit var appColors: AppColors - private lateinit var api: SelfossApi - private lateinit var toolbarMenu: Menu private lateinit var db: AppDatabase - private lateinit var prefs: SharedPreferences private lateinit var binding: ActivityReaderBinding private var activeAlignment: Int = 1 private val JUSTIFY = 1 private val ALIGN_LEFT = 2 + override val di by closestDI() + private val repository : Repository by instance() + private fun showMenuItem(willAddToFavorite: Boolean) { if (willAddToFavorite) { toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE) @@ -68,7 +65,7 @@ class ReaderActivity : AppCompatActivity() { showMenuItem(false) } - private lateinit var editor: SharedPreferences.Editor + private var settings = Settings() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -91,23 +88,8 @@ class ReaderActivity : AppCompatActivity() { supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true) - val settings = - getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) - - prefs = PreferenceManager.getDefaultSharedPreferences(this) - editor = prefs.edit() - - userIdentifier = prefs.getString("unique_id", "")!! - markOnScroll = prefs.getBoolean("mark_on_scroll", false) - activeAlignment = prefs.getInt("text_align", JUSTIFY) - - api = SelfossApi( -// this, -// this@ReaderActivity, -// settings.getBoolean("isSelfSignedCert", false), -// prefs.getString("api_timeout", "-1")!!.toLong() - AndroidApiDetailsService(this@ReaderActivity) - ) + markOnScroll = settings.getBoolean("mark_on_scroll", false) + activeAlignment = settings.getInt("text_align", JUSTIFY) if (allItems.isEmpty()) { finish() @@ -130,8 +112,8 @@ class ReaderActivity : AppCompatActivity() { private fun readItem(item: SelfossModel.Item) { if (markOnScroll) { CoroutineScope(Dispatchers.IO).launch { - api.markAsRead(item.id.toString()) - // TODO: update item in DB + repository.markAsRead(item.id) + // TODO: Handle failure } } } @@ -168,7 +150,8 @@ class ReaderActivity : AppCompatActivity() { } } - private fun alignmentMenu(showJustify: Boolean) { + private fun alignmentMenu() { + val showJustify = activeAlignment == ALIGN_LEFT toolbarMenu.findItem(R.id.align_left).isVisible = !showJustify toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify } @@ -183,11 +166,7 @@ class ReaderActivity : AppCompatActivity() { } else { canFavorite() } - if (activeAlignment == JUSTIFY) { - alignmentMenu(false) - } else { - alignmentMenu(true) - } + alignmentMenu() binding.pager.registerOnPageChangeCallback( object : ViewPager2.OnPageChangeCallback() { @@ -228,34 +207,37 @@ class ReaderActivity : AppCompatActivity() { R.id.star -> { if (allItems[binding.pager.currentItem].starred) { CoroutineScope(Dispatchers.IO).launch { - api.unstarr(allItems[binding.pager.currentItem].id.toString()) - // TODO: update in DB + repository.unstarr(allItems[binding.pager.currentItem].id) + // TODO: Handle failure } afterUnsave() } else { CoroutineScope(Dispatchers.IO).launch { - api.starr(allItems[binding.pager.currentItem].id.toString()) - // TODO: update in DB + repository.starr(allItems[binding.pager.currentItem].id) + // TODO: Handle failure } afterSave() } } R.id.align_left -> { - editor.putInt("text_align", ALIGN_LEFT) - editor.apply() - alignmentMenu(true) + activeAlignment = ALIGN_LEFT + switchAlignmentSetting() refreshFragment() } R.id.align_justify -> { - editor.putInt("text_align", JUSTIFY) - editor.apply() - alignmentMenu(false) + activeAlignment = JUSTIFY + switchAlignmentSetting() refreshFragment() } } return super.onOptionsItemSelected(item) } + private fun switchAlignmentSetting() { + settings.putInt("text_align", activeAlignment) + alignmentMenu() + } + private fun refreshFragment() { finish() overridePendingTransition(0, 0) 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 eb949e0..fe94cf3 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 @@ -1,37 +1,34 @@ package bou.amine.apps.readerforselfossv2.android -import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.os.Bundle -import androidx.preference.PreferenceManager +import android.widget.Toast 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.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.isNetworkAvailable -import bou.amine.apps.readerforselfossv2.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository 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 +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.instance -class SourcesActivity : AppCompatActivity() { +class SourcesActivity : AppCompatActivity(), DIAware { private lateinit var appColors: AppColors private lateinit var binding: ActivitySourcesBinding + override val di by closestDI() + private val repository : Repository by instance() + override fun onCreate(savedInstanceState: Bundle?) { appColors = AppColors(this@SourcesActivity) binding = ActivitySourcesBinding.inflate(layoutInflater) @@ -62,18 +59,6 @@ class SourcesActivity : AppCompatActivity() { super.onResume() val mLayoutManager = LinearLayoutManager(this) - val settings = - 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() - apiDetailsService - ) var items: ArrayList binding.recyclerView.setHasFixedSize(true) @@ -81,11 +66,11 @@ class SourcesActivity : AppCompatActivity() { if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) { CoroutineScope(Dispatchers.Main).launch { - val response = api.sources() + val response = repository.getSources() if (response != null) { items = response - val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api, - apiDetailsService + val mAdapter = SourcesListAdapter( + this@SourcesActivity, items ) binding.recyclerView.adapter = mAdapter mAdapter.notifyDataSetChanged() 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 368e7d7..c0b8db6 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 @@ -17,32 +17,28 @@ import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActiv 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.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository 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 +import org.kodein.di.DI +import org.kodein.di.android.closestDI +import org.kodein.di.instance class ItemCardAdapter( override val app: Activity, override var items: ArrayList, - override val api: SelfossApi, - override val apiDetailsService: ApiDetailsService, override val db: AppDatabase, private val helper: CustomTabActivityHelper, private val internalBrowser: Boolean, private val articleViewer: Boolean, private val fullHeightCards: Boolean, override val appColors: AppColors, - override val userIdentifier: String, override val config: Config, - override val searchService: SearchService, override val updateItems: (ArrayList) -> Unit ) : ItemsAdapter() { private val c: Context = app.baseContext @@ -50,6 +46,9 @@ class ItemCardAdapter( private val imageMaxHeight: Int = c.resources.getDimension(R.dimen.card_image_max_height).toInt() + override val di: DI by closestDI(app) + override val repository : Repository by instance() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(binding) @@ -66,23 +65,23 @@ class ItemCardAdapter( binding.title.setLinkTextColor(appColors.colorAccent) - binding.sourceTitleAndDate.text = itm.sourceAndDateText(DateUtils(apiDetailsService)) + binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils) if (!fullHeightCards) { binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.scaleType = ScaleType.CENTER_CROP } - if (itm.getThumbnail(apiDetailsService.getBaseUrl()).isEmpty()) { + if (itm.getThumbnail(repository.baseUrl).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(apiDetailsService.getBaseUrl()), binding.itemImage) + c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage) } - if (itm.getIcon(apiDetailsService.getBaseUrl()).isEmpty()) { + if (itm.getIcon(repository.baseUrl).isEmpty()) { val color = generator.getColor(itm.getSourceTitle()) val drawable = @@ -92,7 +91,7 @@ class ItemCardAdapter( .build(itm.getSourceTitle().toTextDrawableString(c), color) binding.sourceImage.setImageDrawable(drawable) } else { - c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.sourceImage) + c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.sourceImage) } } } @@ -114,15 +113,15 @@ class ItemCardAdapter( if (c.isNetworkAvailable()) { if (item.starred) { CoroutineScope(Dispatchers.IO).launch { - api.unstarr(item.id.toString()) - // TODO: save to db + repository.unstarr(item.id) + // TODO: Handle failure } item.starred = false binding.favButton.isSelected = false } else { CoroutineScope(Dispatchers.IO).launch { - api.starr(item.id.toString()) - // TODO: save to db + repository.starr(item.id) + // TODO: Handle failure } item.starred = true binding.favButton.isSelected = true @@ -152,8 +151,7 @@ class ItemCardAdapter( customTabsIntent, internalBrowser, articleViewer, - app, - searchService + app ) } } 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 e4c52fd..b48d358 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 @@ -13,32 +13,31 @@ 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.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository 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 org.kodein.di.DI +import org.kodein.di.android.closestDI +import org.kodein.di.instance class ItemListAdapter( override val app: Activity, override var items: ArrayList, - override val api: SelfossApi, - override val apiDetailsService: ApiDetailsService, override val db: AppDatabase, private val helper: CustomTabActivityHelper, private val internalBrowser: Boolean, private val articleViewer: Boolean, - override val userIdentifier: String, override val appColors: AppColors, override val config: Config, - override val searchService: SearchService, override val updateItems: (ArrayList) -> Unit ) : ItemsAdapter() { private val generator: ColorGenerator = ColorGenerator.MATERIAL private val c: Context = app.baseContext + override val di: DI by closestDI(app) + override val repository : Repository by instance() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(binding) @@ -54,11 +53,11 @@ class ItemListAdapter( binding.title.setLinkTextColor(appColors.colorAccent) - binding.sourceTitleAndDate.text = itm.sourceAndDateText(DateUtils(apiDetailsService)) + binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils) - if (itm.getThumbnail(apiDetailsService.getBaseUrl()).isEmpty()) { + if (itm.getThumbnail(repository.baseUrl).isEmpty()) { - if (itm.getIcon(apiDetailsService.getBaseUrl()).isEmpty()) { + if (itm.getIcon(repository.baseUrl).isEmpty()) { val color = generator.getColor(itm.getSourceTitle()) val drawable = @@ -69,10 +68,10 @@ class ItemListAdapter( binding.itemImage.setImageDrawable(drawable) } else { - c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage) + c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage) } } else { - c.bitmapCenterCrop(config, itm.getThumbnail(apiDetailsService.getBaseUrl()), binding.itemImage) + c.bitmapCenterCrop(config, itm.getThumbnail(repository.baseUrl), binding.itemImage) } } } @@ -97,8 +96,7 @@ class ItemListAdapter( customTabsIntent, internalBrowser, articleViewer, - app, - searchService + app ) } } 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 82a3cd1..07d3d80 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 @@ -8,25 +8,22 @@ import bou.amine.apps.readerforselfossv2.android.R 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.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository 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.ItemType import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kodein.di.DIAware -abstract class ItemsAdapter : RecyclerView.Adapter() { +abstract class ItemsAdapter : RecyclerView.Adapter(), DIAware { abstract var items: ArrayList - abstract val api: SelfossApi - abstract val apiDetailsService: ApiDetailsService + abstract val repository: Repository abstract val db: AppDatabase - abstract val userIdentifier: String abstract val app: Activity abstract val appColors: AppColors abstract val config: Config - abstract val searchService: SearchService abstract val updateItems: (ArrayList) -> Unit fun updateAllItems(items: ArrayList) { @@ -82,18 +79,15 @@ abstract class ItemsAdapter : RecyclerView.Adapte private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) { val i = items[position] CoroutineScope(Dispatchers.IO).launch { - api.markAsRead(i.id.toString()) - // TODO: update db - + repository.markAsRead(i.id) + } + if (repository.displayedItems == ItemType.UNREAD) { + items.remove(i) + notifyItemRemoved(position) + updateItems(items) + } else { + notifyItemChanged(position) } - // Todo: -// if (SharedItems.displayedItems == "unread") { -// items.remove(i) -// notifyItemRemoved(position) -// updateItems(items) -// } else { -// notifyItemChanged(position) -// } if (showSnackbar) { unmarkSnackbar(i, position) } @@ -101,7 +95,7 @@ abstract class ItemsAdapter : RecyclerView.Adapte private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) { CoroutineScope(Dispatchers.IO).launch { - api.unmarkAsRead(items[position].id.toString()) + repository.unmarkAsRead(items[position].id) // Todo: SharedItems.unreadItem(app, api, db, items[position]) // TODO: update db 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 b49168e..30c9260 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 @@ -16,26 +16,30 @@ 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.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString -import bou.amine.apps.readerforselfossv2.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.android.closestDI +import org.kodein.di.instance class SourcesListAdapter( private val app: Activity, - private val items: ArrayList, - private val api: SelfossApi, - private val apiDetailsService: ApiDetailsService -) : RecyclerView.Adapter() { + private val items: ArrayList +) : RecyclerView.Adapter(), DIAware { private val c: Context = app.baseContext private val generator: ColorGenerator = ColorGenerator.MATERIAL private lateinit var config: Config private lateinit var binding: SourceListItemBinding + override val di: DI by closestDI(app) + private val repository : Repository by instance() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder(binding.root) @@ -43,9 +47,9 @@ class SourcesListAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { val itm = items[position] - config = Config(c) + config = Config() - if (itm.getIcon(apiDetailsService.getBaseUrl()).isEmpty()) { + if (itm.getIcon(repository.baseUrl).isEmpty()) { val color = generator.getColor(itm.getTitleDecoded()) val drawable = @@ -55,7 +59,7 @@ class SourcesListAdapter( .build(itm.getTitleDecoded().toTextDrawableString(c), color) binding.itemImage.setImageDrawable(drawable) } else { - c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage) + c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage) } binding.sourceTitle.text = itm.getTitleDecoded() @@ -77,8 +81,8 @@ class SourcesListAdapter( if (c.isNetworkAvailable(null)) { val (id) = items[adapterPosition] CoroutineScope(Dispatchers.IO).launch { - val action = api.deleteSource(id) - if (action != null && action.isSuccess) { + val successfullyDeletedSource = repository.deleteSource(id) + if (successfullyDeletedSource) { items.removeAt(adapterPosition) notifyItemRemoved(adapterPosition) notifyItemRangeChanged(adapterPosition, itemCount) 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 e929f99..860d2c1 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 @@ -5,7 +5,6 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build -import androidx.preference.PreferenceManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT import androidx.core.app.NotificationCompat.PRIORITY_LOW @@ -13,52 +12,39 @@ import androidx.room.Room import androidx.work.Worker import androidx.work.WorkerParameters import bou.amine.apps.readerforselfossv2.android.MainActivity +import bou.amine.apps.readerforselfossv2.android.MyApp import bou.amine.apps.readerforselfossv2.android.R 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.network.isNetworkAvailable - -import bou.amine.apps.readerforselfossv2.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.repository.Repository import bou.amine.apps.readerforselfossv2.rest.SelfossModel -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.ItemType +import com.russhwolf.settings.Settings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kodein.di.DIAware +import org.kodein.di.instance import java.util.* import kotlin.concurrent.schedule import kotlin.concurrent.thread -class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params) { +class LoadingWorker(val context: Context, params: WorkerParameters) : Worker(context, params), DIAware { lateinit var db: AppDatabase -override fun doWork(): Result { - val settings = - this.context.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) - val sharedPref = PreferenceManager.getDefaultSharedPreferences(this.context) - val periodicRefresh = sharedPref.getBoolean("periodic_refresh", false) - if (periodicRefresh) { - val apiDetailsService = AndroidApiDetailsService(this.context) - val api = SelfossApi( -// this.context, -// null, -// settings.getBoolean("isSelfSignedCert", false), -// sharedPref.getString("api_timeout", "-1")!!.toLong() - apiDetailsService - ) + override val di by lazy { (applicationContext as MyApp).di } + private val repository : Repository by instance() - val dateUtils = DateUtils(apiDetailsService) - val searchService = SearchService(dateUtils) - val service = SelfossService(api, AndroidDeviceDatabaseService(AndroidDeviceDatabase(applicationContext), searchService), searchService) +override fun doWork(): Result { + val settings = Settings() + val periodicRefresh = settings.getBoolean("periodic_refresh", false) + if (periodicRefresh) { if (context.isNetworkAvailable()) { @@ -77,7 +63,7 @@ override fun doWork(): Result { notificationManager.notify(1, notification.build()) - val notifyNewItems = sharedPref.getBoolean("notify_new_items", false) + val notifyNewItems = settings.getBoolean("notify_new_items", false) db = Room.databaseBuilder( applicationContext, @@ -90,19 +76,19 @@ override fun doWork(): Result { actions.forEach { action -> when { action.read -> doAndReportOnFail( - api.markAsRead(action.articleId), + repository.markAsRead(action.articleId.toInt()), action ) action.unread -> doAndReportOnFail( - api.unmarkAsRead(action.articleId), + repository.unmarkAsRead(action.articleId.toInt()), action ) action.starred -> doAndReportOnFail( - api.starr(action.articleId), + repository.starr(action.articleId.toInt()), action ) action.unstarred -> doAndReportOnFail( - api.unstarr(action.articleId), + repository.unstarr(action.articleId.toInt()), action ) } @@ -111,10 +97,10 @@ override fun doWork(): Result { if (context.isNetworkAvailable()) { launch { try { - val newItems = service.allNewItems() + val newItems = repository.allItems(ItemType.UNREAD) handleNewItemsNotification(newItems, notifyNewItems, notificationManager) - val readItems = service.allReadItems() - val starredItems = service.allStarredItems() + val readItems = repository.allItems(ItemType.ALL) + val starredItems = repository.allItems(ItemType.STARRED) // TODO: save all to DB } catch (e: Throwable) {} } @@ -173,8 +159,9 @@ override fun doWork(): Result { } } - private fun doAndReportOnFail(result: SelfossModel.SuccessResponse?, action: ActionEntity) { - if (result != null && result.isSuccess) { + private fun doAndReportOnFail(result: Boolean, action: ActionEntity) { + // TODO: Failures should be reported + if (result) { thread { db.actionsDao().delete(action) } 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 c9628aa..0deb6a1 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 @@ -1,8 +1,6 @@ package bou.amine.apps.readerforselfossv2.android.fragments -import android.content.Context import android.content.Intent -import android.content.SharedPreferences import android.content.res.ColorStateList import android.content.res.TypedArray import android.graphics.Bitmap @@ -11,14 +9,16 @@ import android.graphics.drawable.ColorDrawable import android.net.Uri import android.os.Bundle import android.view.* -import android.webkit.* +import android.webkit.WebResourceResponse +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.Toast 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 @@ -26,35 +26,32 @@ import bou.amine.apps.readerforselfossv2.android.api.mercury.MercuryApi import bou.amine.apps.readerforselfossv2.android.api.mercury.ParsedContent 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.getBitmapInputStream 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.repository.Repository 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 com.russhwolf.settings.Settings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kodein.di.DI +import org.kodein.di.DIAware +import org.kodein.di.android.x.closestDI +import org.kodein.di.instance import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -63,10 +60,7 @@ import java.net.URL import java.util.* import java.util.concurrent.ExecutionException -class ArticleFragment : Fragment() { - private lateinit var dbService: AndroidDeviceDatabaseService - private lateinit var apiDetailsService: ApiDetailsService - private lateinit var service: SelfossService +class ArticleFragment : Fragment(), DIAware { private var fontSize: Int = 16 private lateinit var item: SelfossModel.Item private var mCustomTabActivityHelper: CustomTabActivityHelper? = null @@ -76,7 +70,6 @@ class ArticleFragment : Fragment() { private lateinit var contentImage: String private lateinit var contentTitle: String private lateinit var allImages : ArrayList - private lateinit var editor: SharedPreferences.Editor private lateinit var fab: FloatingActionButton private lateinit var appColors: AppColors private lateinit var db: AppDatabase @@ -85,7 +78,10 @@ class ArticleFragment : Fragment() { private var _binding: FragmentArticleBinding? = null private val binding get() = _binding!! - private lateinit var prefs: SharedPreferences + override val di : DI by closestDI() + private val repository: Repository by instance() + + private var settings = Settings() private var typeface: Typeface? = null private var resId: Int = 0 @@ -101,16 +97,10 @@ class ArticleFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { appColors = AppColors(requireActivity()) - config = Config(requireActivity()) + config = Config() super.onCreate(savedInstanceState) - apiDetailsService = AndroidApiDetailsService(requireContext()) - - dbService = AndroidDeviceDatabaseService(AndroidDeviceDatabase(requireContext()), SearchService(DateUtils(apiDetailsService))) - - service = SelfossService(SelfossApi(apiDetailsService), dbService, SearchService(DateUtils(apiDetailsService))) - val pi: ParecelableItem = requireArguments().getParcelable(ARG_ITEMS)!! item = pi.toModel() @@ -132,16 +122,14 @@ class ArticleFragment : Fragment() { url = item.getLinkDecoded() contentText = item.content contentTitle = item.getTitleDecoded() - contentImage = item.getThumbnail(apiDetailsService.getBaseUrl()) - contentSource = item.sourceAndDateText(DateUtils(apiDetailsService)) + contentImage = item.getThumbnail(repository.baseUrl) + contentSource = item.sourceAndDateText(repository.dateUtils) allImages = item.getImages() - prefs = PreferenceManager.getDefaultSharedPreferences(activity) - editor = prefs.edit() - fontSize = prefs.getString("reader_font_size", "16")!!.toInt() - staticBar = prefs.getBoolean("reader_static_bar", false) + fontSize = settings.getString("reader_font_size", "16").toInt() + staticBar = settings.getBoolean("reader_static_bar", false) - font = prefs.getString("reader_font", "")!! + font = settings.getString("reader_font", "") if (font.isNotEmpty()) { resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName) typeface = try { @@ -155,16 +143,6 @@ class ArticleFragment : Fragment() { refreshAlignment() - val settings = requireActivity().getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) - - val api = SelfossApi( -// requireContext(), -// requireActivity(), -// settings.getBoolean("isSelfSignedCert", false), -// prefs.getString("api_timeout", "-1")!!.toLong() - apiDetailsService - ) - fab = binding.fab fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent) @@ -191,8 +169,7 @@ class ArticleFragment : Fragment() { R.id.unread_action -> if (context != null) { if (this@ArticleFragment.item.unread) { CoroutineScope(Dispatchers.IO).launch { - api.markAsRead(this@ArticleFragment.item.id.toString()) - // TODO: Update in DB + repository.markAsRead(this@ArticleFragment.item.id) } this@ArticleFragment.item.unread = false Toast.makeText( @@ -202,8 +179,7 @@ class ArticleFragment : Fragment() { ).show() } else { CoroutineScope(Dispatchers.IO).launch { - api.unmarkAsRead(this@ArticleFragment.item.id.toString()) - // TODO: Update in DB + repository.unmarkAsRead(this@ArticleFragment.item.id) } this@ArticleFragment.item.unread = true Toast.makeText( @@ -276,10 +252,7 @@ class ArticleFragment : Fragment() { .setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setPositiveButton(android.R.string.ok ) { _, _ -> - val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val editor = sharedPref.edit() - editor.putBoolean("prefer_article_viewer", false) - editor.apply() + settings.putBoolean("prefer_article_viewer", false) requireActivity().finish() } .create() @@ -295,7 +268,7 @@ class ArticleFragment : Fragment() { } private fun refreshAlignment() { - textAlignment = when (prefs.getInt("text_align", 1)) { + textAlignment = when (settings.getInt("text_align", 1)) { 1 -> "justify" 2 -> "left" else -> "justify" diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt index aa708e7..f4382ce 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/settings/SettingsActivity.kt @@ -17,6 +17,7 @@ import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySettingsBin import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.utils.Config import com.ftinc.scoop.Scoop +import com.russhwolf.settings.Settings import java.lang.NumberFormatException private const val TITLE_TAG = "settingsActivityTitle" @@ -173,14 +174,12 @@ class SettingsActivity : AppCompatActivity(), override fun onOptionsItemSelected(item: MenuItem): Boolean { val id = item.itemId if (id == R.id.clear) { - val pref = PreferenceManager.getDefaultSharedPreferences(activity) - val editor = pref.edit() - editor.remove("color_primary") - editor.remove("color_primary_dark") - editor.remove("color_accent") - editor.remove("color_accent_dark") - editor.remove("dark_theme") - editor.apply() + val settings = Settings() + settings.remove("color_primary") + settings.remove("color_primary_dark") + settings.remove("color_accent") + settings.remove("color_accent_dark") + settings.remove("dark_theme") requireActivity().recreate() } return super.onOptionsItemSelected(item) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/themes/AppColors.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/themes/AppColors.kt index 321c400..9c10e52 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/themes/AppColors.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/themes/AppColors.kt @@ -2,8 +2,8 @@ package bou.amine.apps.readerforselfossv2.android.themes import android.app.Activity import androidx.annotation.ColorInt -import androidx.preference.PreferenceManager import bou.amine.apps.readerforselfossv2.android.R +import com.russhwolf.settings.Settings class AppColors(a: Activity) { @@ -16,30 +16,30 @@ class AppColors(a: Activity) { val isDarkTheme: Boolean init { - val sharedPref = PreferenceManager.getDefaultSharedPreferences(a) + val settings = Settings() colorPrimary = - sharedPref.getInt( + settings.getInt( "color_primary", a.resources.getColor(R.color.colorPrimary) ) colorPrimaryDark = - sharedPref.getInt( + settings.getInt( "color_primary_dark", a.resources.getColor(R.color.colorPrimaryDark) ) colorAccent = - sharedPref.getInt( + settings.getInt( "color_accent", a.resources.getColor(R.color.colorAccent) ) colorAccentDark = - sharedPref.getInt( + settings.getInt( "color_accent_dark", a.resources.getColor(R.color.colorAccentDark) ) isDarkTheme = - sharedPref.getBoolean( + settings.getBoolean( "dark_theme", false ) diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/Config.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/Config.kt index 849c261..ba81e76 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/Config.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/Config.kt @@ -3,28 +3,27 @@ package bou.amine.apps.readerforselfossv2.android.utils import android.app.Activity import android.content.Context import android.content.Intent -import android.content.SharedPreferences -import androidx.preference.PreferenceManager import bou.amine.apps.readerforselfossv2.android.LoginActivity +import com.russhwolf.settings.Settings -class Config(c: Context) { +class Config { - val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(c) + val settings = Settings() val baseUrl: String - get() = settings.getString("url", "")!! + get() = settings.getString("url", "") val userLogin: String - get() = settings.getString("login", "")!! + get() = settings.getString("login", "") val userPassword: String - get() = settings.getString("password", "")!! + get() = settings.getString("password", "") val httpUserLogin: String - get() = settings.getString("httpUserName", "")!! + get() = settings.getString("httpUserName", "") val httpUserPassword: String - get() = settings.getString("httpPassword", "")!! + get() = settings.getString("httpPassword", "") companion object { const val settingsName = "paramsselfoss" @@ -47,11 +46,10 @@ class Config(c: Context) { fun logoutAndRedirect( c: Context, callingActivity: Activity, - editor: SharedPreferences.Editor, baseUrlFail: Boolean = false ): Boolean { - val settings = PreferenceManager.getDefaultSharedPreferences(c) - settings.edit().clear().commit() + val settings = Settings() + settings.clear() val intent = Intent(c, LoginActivity::class.java) if (baseUrlFail) { intent.putExtra("baseUrlFail", baseUrlFail) 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 b642c70..7e0200a 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 @@ -10,18 +10,17 @@ import android.net.Uri import android.os.Build import android.text.Spannable import android.text.style.ClickableSpan -import androidx.browser.customtabs.CustomTabsIntent import android.util.Patterns import android.view.MotionEvent import android.view.View import android.widget.TextView import android.widget.Toast +import androidx.browser.customtabs.CustomTabsIntent import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.ReaderActivity 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 @@ -79,12 +78,10 @@ fun Context.openItemUrlInternally( linkDecoded: String, customTabsIntent: CustomTabsIntent, articleViewer: Boolean, - app: Activity, - searchService: SearchService + app: Activity ) { if (articleViewer) { ReaderActivity.allItems = allItems - searchService.position = currentItem val intent = Intent(this, ReaderActivity::class.java) intent.putExtra("currentItem", currentItem) app.startActivity(intent) @@ -123,8 +120,7 @@ fun Context.openItemUrl( customTabsIntent: CustomTabsIntent, internalBrowser: Boolean, articleViewer: Boolean, - app: Activity, - searchService: SearchService + app: Activity ) { if (!linkDecoded.isUrlValid()) { @@ -143,8 +139,7 @@ fun Context.openItemUrl( linkDecoded, customTabsIntent, articleViewer, - app, - searchService + app ) } else { this.openItemUrlInternalBrowser( diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/SelfSignedGlideModule.kt b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/SelfSignedGlideModule.kt index 8449013..999a130 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/SelfSignedGlideModule.kt +++ b/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/utils/glide/SelfSignedGlideModule.kt @@ -1,13 +1,13 @@ package bou.amine.apps.readerforselfossv2.android.utils.glide import android.content.Context -import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.getUnsafeHttpClient import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import com.bumptech.glide.Registry import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.module.GlideModule +import com.russhwolf.settings.Settings import java.io.InputStream class SelfSignedGlideModule : GlideModule { @@ -18,8 +18,8 @@ class SelfSignedGlideModule : GlideModule { override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) { if (context != null) { - val pref = context?.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) - if (pref.getBoolean("isSelfSignedCert", false)) { + val settings = Settings() + if (settings.getBoolean("isSelfSignedCert", false)) { val client = getUnsafeHttpClient().build() registry?.append( diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index f09de7c..d5062d4 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -27,6 +27,15 @@ kotlin { 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") + + //Dependency Injection + implementation("org.kodein.di:kodein-di:7.12.0") + + //Settings + implementation("com.russhwolf:multiplatform-settings-no-arg:0.9") + + //Logging + implementation("io.github.aakira:napier:2.6.1") } } val commonTest by getting { diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/DI/modules.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/DI/modules.kt new file mode 100644 index 0000000..2beba39 --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/DI/modules.kt @@ -0,0 +1,14 @@ +package bou.amine.apps.readerforselfossv2.DI + +import bou.amine.apps.readerforselfossv2.rest.SelfossApi +import bou.amine.apps.readerforselfossv2.service.ApiDetailsService +import bou.amine.apps.readerforselfossv2.service.ApiDetailsServiceImpl +import org.kodein.di.DI +import org.kodein.di.bind +import org.kodein.di.instance +import org.kodein.di.singleton + +val networkModule by DI.Module { + bind() with singleton { ApiDetailsServiceImpl() } + bind() with singleton { SelfossApi(instance()) } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt new file mode 100644 index 0000000..c9fed74 --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/repository/RepositoryImpl.kt @@ -0,0 +1,285 @@ +package bou.amine.apps.readerforselfossv2.repository + +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.utils.DateUtils +import bou.amine.apps.readerforselfossv2.utils.ItemType +import com.russhwolf.settings.Settings +import io.github.aakira.napier.Napier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService) { + val settings = Settings() + + var items = ArrayList() + + var baseUrl = apiDetails.getBaseUrl() + lateinit var dateUtils: DateUtils + + var displayedItems = ItemType.UNREAD + + var tagFilter: SelfossModel.Tag? = null + var sourceFilter: SelfossModel.Source? = null + var searchFilter: String? = null + + var itemsCaching = settings.getBoolean("items_caching", false) + + var apiMajorVersion = 0 + var badgeUnread = 0 + set(value) {field = if (value < 0) { 0 } else { value } } + var badgeAll = 0 + set(value) {field = if (value < 0) { 0 } else { value } } + var badgeStarred = 0 + set(value) {field = if (value < 0) { 0 } else { value } } + + init { + // TODO: Dispatchers.IO not available in KMM, an alternative solution should be found + CoroutineScope(Dispatchers.Main).launch { + updateApiVersion() + dateUtils = DateUtils(apiMajorVersion) + reloadBadges() + } + } + + suspend fun getNewerItems(): ArrayList { + // TODO: Check connectivity, use the updatedSince parameter + val fetchedItems = api.getItems(displayedItems.type, + settings.getString("prefer_api_items_number", "200").toInt(), + offset = 0, + tagFilter?.tag, + sourceFilter?.id?.toLong(), + searchFilter, + null) + + if (fetchedItems != null) { + items = ArrayList(fetchedItems) + } + return items + } + + suspend fun getOlderItems(): ArrayList { + // TODO: Check connectivity + val offset = items.size + val fetchedItems = api.getItems(displayedItems.type, + settings.getString("prefer_api_items_number", "200").toInt(), + offset, + tagFilter?.tag, + sourceFilter?.id?.toLong(), + searchFilter, + null) + + if (fetchedItems != null) { + appendItems(fetchedItems) + } + return items + } + + suspend fun allItems(itemType: ItemType): List? = + api.getItems(itemType.type, 200, 0, tagFilter?.tag, sourceFilter?.id?.toLong(), searchFilter, null) + + private fun appendItems(fetchedItems: List) { + // TODO: Store in DB if enabled by user + val fetchedIDS = fetchedItems.map { it.id } + val tmpItems = ArrayList(items.filterNot { it.id in fetchedIDS }) + tmpItems.addAll(fetchedItems) + sortItems(tmpItems) + items = tmpItems + } + + private fun sortItems(items: ArrayList) { + items.sortByDescending { dateUtils.parseDate(it.datetime) } + } + + suspend fun reloadBadges(): Boolean { + // TODO: Check connectivity, calculate from DB + var success = false + val response = api.stats() + if (response != null) { + badgeUnread = response.unread + badgeAll = response.total + badgeStarred = response.starred + success = true + } + return success + } + + suspend fun getTags(): List? { + // TODO: Check success, store in DB + return api.tags() + } + + suspend fun getSpouts(): Map? { + // TODO: Check success, store in DB + return api.spouts() + } + + suspend fun getSources(): ArrayList? { + // TODO: Check success + return api.sources() + } + + suspend fun markAsRead(id: Int): Boolean { + // TODO: Check internet connection + val success = api.markAsRead(id.toString())?.isSuccess == true + + if (success) { + markAsReadLocally(items.first {it.id == id}) + } + return success + } + + suspend fun unmarkAsRead(id: Int): Boolean { + // TODO: Check internet connection + val success = api.unmarkAsRead(id.toString())?.isSuccess == true + + if (success) { + unmarkAsReadLocally(items.first {it.id == id}) + } + return success + } + + suspend fun starr(id: Int): Boolean { + // TODO: Check success, store in DB + val success = api.starr(id.toString())?.isSuccess == true + + if (success) { + starrLocally(items.first {it.id == id}) + } + return success + } + + suspend fun unstarr(id: Int): Boolean { + // TODO: Check internet connection + val success = api.unstarr(id.toString())?.isSuccess == true + + if (success) { + unstarrLocally(items.first {it.id == id}) + } + return success + } + + suspend fun markAllAsRead(ids: List): Boolean { + // TODO: Check Internet connectivity, store in DB + + val success = api.markAllAsRead(ids.map { it.toString() })?.isSuccess == true + + if (success) { + val itemsToMark = items.filter { it.id in ids } + for (item in itemsToMark) { + markAsReadLocally(item) + } + } + return success + } + + private fun markAsReadLocally(item: SelfossModel.Item) { + // TODO: Mark also in the database + if (item.unread) { + item.unread = false + badgeUnread -= 1 + } + } + + private fun unmarkAsReadLocally(item: SelfossModel.Item) { + // TODO: Mark also in the database + if (!item.unread) { + item.unread = true + badgeUnread += 1 + } + } + + private fun starrLocally(item: SelfossModel.Item) { + // TODO: Mark also in the database + if (!item.starred) { + item.starred = true + badgeStarred += 1 + } + } + + private fun unstarrLocally(item: SelfossModel.Item) { + // TODO: Mark also in the database + if (item.starred) { + item.starred = false + badgeStarred -= 1 + } + } + + suspend fun createSource( + title: String, + url: String, + spout: String, + tags: String, + filter: String + ): Boolean { + // TODO: Check connectivity + val response = api.createSourceForVersion( + title, + url, + spout, + tags, + filter, + apiMajorVersion + ) + + return response != null + } + + suspend fun deleteSource(id: Int): Boolean { + // TODO: Check connectivity, store in DB + var success = false + val response = api.deleteSource(id) + if (response != null) { + success = response.isSuccess + } + + return success + } + + suspend fun updateRemote(): Boolean { + // TODO: Handle connectivity issues + val response = api.update() + return response?.isSuccess ?: false + } + + suspend fun login(): Boolean { + var result = false + try { + val response = api.login() + if (response != null && response.isSuccess) { + result = true + } + } catch (cause: Throwable) { + Napier.e(cause.stackTraceToString(),tag = "RepositoryImpl.updateRemote") + } + return result + } + + fun refreshLoginInformation(url: String, login: String, password: String, + httpLogin: String, httpPassword: String, + isSelfSignedCert: Boolean) { + settings.putString("url", url) + settings.putString("login", login) + settings.putString("httpUserName", httpLogin) + settings.putString("password", password) + settings.putString("httpPassword", httpPassword) + settings.putBoolean("isSelfSignedCert", isSelfSignedCert) + baseUrl = url + api.refreshLoginInformation() + } + + private suspend fun updateApiVersion() { + // TODO: Handle connectivity issues + val fetchedVersion = api.version() + if (fetchedVersion != null) { + apiMajorVersion = fetchedVersion.getApiMajorVersion() + settings.putInt("apiVersionMajor", apiMajorVersion) + } else { + apiMajorVersion = settings.getInt("apiVersionMajor", 0) + } + } + + // TODO: Handle offline actions +} \ No newline at end of file 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 23fc2bd..90ba822 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 @@ -3,10 +3,6 @@ package bou.amine.apps.readerforselfossv2.rest import bou.amine.apps.readerforselfossv2.service.ApiDetailsService import io.ktor.client.* 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.plugins.cache.* import io.ktor.client.request.* import io.ktor.client.request.forms.* @@ -18,43 +14,51 @@ import kotlinx.serialization.json.Json class SelfossApi(private val apiDetailsService: ApiDetailsService) { - private val client = HttpClient() { - install(ContentNegotiation) { - install(HttpCache) - 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()) { + var client = createHttpClient() - install(Auth) { - basic { - credentials { - BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword()) - } - sendWithoutRequest { - true + private fun createHttpClient(): HttpClient { + return HttpClient { + install(ContentNegotiation) { + install(HttpCache) + json(Json { + prettyPrint = true + isLenient = true + ignoreUnknownKeys = true + }) + } + install(Logging) { + logger = object : Logger { + override fun log(message: String) { + apiDetailsService.logApiCalls(message) } } + level = LogLevel.ALL } - }*/ - expectSuccess = false + /* 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) = + fun url(path: String) = "${apiDetailsService.getBaseUrl()}$path" + fun refreshLoginInformation() { + apiDetailsService.refresh() + client = createHttpClient() + } suspend fun login(): SelfossModel.SuccessResponse? = client.get(url("/login")) { @@ -66,10 +70,10 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) { type: String, items: Int, offset: Int, - tag: String? = "", - source: Long? = null, - search: String? = "", - updatedSince: String? = "" + tag: String?, + source: Long?, + search: String?, + updatedSince: String? ): List? = client.get(url("/items")) { parameter("username", apiDetailsService.getUserName()) @@ -164,7 +168,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) { createSource(title, url, spout, tags, filter) } - private suspend fun createSource( + suspend fun createSource( title: String, url: String, spout: String, @@ -182,7 +186,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) { } ).body() - private suspend fun createSource2( + suspend fun createSource2( title: String, url: String, spout: String, 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 index 77214c9..48e477e 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsService.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsService.kt @@ -6,4 +6,5 @@ interface ApiDetailsService { fun getBaseUrl(): String fun getUserName(): String fun getPassword(): String + fun refresh() } \ No newline at end of file diff --git a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/service/AndroidApiDetailsService.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsServiceImpl.kt similarity index 50% rename from androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/service/AndroidApiDetailsService.kt rename to shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsServiceImpl.kt index 77e6e31..6256094 100644 --- a/androidApp/src/main/java/bou/amine/apps/readerforselfossv2/android/service/AndroidApiDetailsService.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/ApiDetailsServiceImpl.kt @@ -1,48 +1,52 @@ -package bou.amine.apps.readerforselfossv2.android.service +package bou.amine.apps.readerforselfossv2.service -import android.content.Context -import android.content.SharedPreferences -import android.util.Log -import androidx.preference.PreferenceManager -import bou.amine.apps.readerforselfossv2.service.ApiDetailsService +import com.russhwolf.settings.Settings +import io.github.aakira.napier.Napier -class AndroidApiDetailsService(c: Context) : ApiDetailsService { - val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(c) +class ApiDetailsServiceImpl : ApiDetailsService { + val settings: Settings = Settings() 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) + Napier.d(message, tag = "LogApiCalls") } override fun getApiVersion(): Int { if (_apiVersion == -1) { - _apiVersion = settings.getInt("apiVersionMajor", -1)!! - return settings.getInt("apiVersionMajor", -1)!! + _apiVersion = settings.getInt("apiVersionMajor", -1) + return _apiVersion } return _apiVersion } override fun getBaseUrl(): String { if (_baseUrl.isEmpty()) { - _baseUrl = settings.getString("url", "")!! + _baseUrl = settings.getString("url", "") } return _baseUrl } override fun getUserName(): String { if (_userName.isEmpty()) { - _userName = settings.getString("login", "")!! + _userName = settings.getString("login", "") } return _userName } override fun getPassword(): String { if (_password.isEmpty()) { - _password = settings.getString("password", "")!! + _password = settings.getString("password", "") } return _password } + + override fun refresh() { + _password = settings.getString("password", "") + _userName = settings.getString("login", "") + _baseUrl = settings.getString("url", "") + _apiVersion = settings.getInt("apiVersionMajor", -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 deleted file mode 100644 index 365abe9..0000000 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/service/SelfossService.kt +++ /dev/null @@ -1,109 +0,0 @@ -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 getReadItems(itemsNumber: Int, offset: Int, isNetworkAvailable: Boolean): List? = withContext( - Dispatchers.Default) { - if (isNetworkAvailable) { - val apiItems = readItems( itemsNumber, offset) - // SAVE OR UPDATE IN DB - return@withContext apiItems - } else { - // GET FROM DB - return@withContext emptyList() - } - } - - suspend fun getUnreadItems(itemsNumber: Int, offset: Int, isNetworkAvailable: Boolean): List? = withContext( - Dispatchers.Default) { - if (isNetworkAvailable) { - val apiItems = newItems(itemsNumber, offset) - // SAVE OR UPDATE IN DB - return@withContext apiItems - } else { - // GET FROM DB - return@withContext emptyList() - } - } - - suspend fun getStarredItems(itemsNumber: Int, offset: Int, isNetworkAvailable: Boolean): List? = withContext( - Dispatchers.Default) { - if (isNetworkAvailable) { - val apiItems = starredItems(itemsNumber, offset) - // SAVE OR UPDATE IN DB - return@withContext apiItems - } else { - // GET FROM DB - return@withContext emptyList() - } - } - - suspend fun readAll(ids: List, isNetworkAvailable: Boolean): Boolean { - // Add ids params - var success = false - if (isNetworkAvailable) { - success = api.markAllAsRead(ids)?.isSuccess == true - // SAVE OR UPDATE IN DB - } - // 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) - } - } - - suspend fun allNewItems(): List? = - readItems(200, 0) - - suspend fun allReadItems(): List? = - newItems(200, 0) - - 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 index 2bafad3..0af9e4f 100644 --- a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/DateUtils.kt @@ -2,7 +2,6 @@ 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 @@ -15,12 +14,12 @@ fun SelfossModel.Item.parseDate(dateUtils: bou.amine.apps.readerforselfossv2.uti fun SelfossModel.Item.parseRelativeDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): String = dateUtils.parseRelativeDate(this.datetime) -class DateUtils(private val apiDetailsService: ApiDetailsService) { +class DateUtils(private val apiMajorVersion: Int) { fun parseDate(dateString: String): Instant { val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss" - return if (apiDetailsService.getApiVersion() >= 4) { + return if (apiMajorVersion >= 4) { OffsetDateTime.parse(dateString).toInstant() } else { LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC) diff --git a/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/Enums.kt b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/Enums.kt new file mode 100644 index 0000000..fd610d2 --- /dev/null +++ b/shared/src/commonMain/kotlin/bou/amine/apps/readerforselfossv2/utils/Enums.kt @@ -0,0 +1,11 @@ +package bou.amine.apps.readerforselfossv2.utils + +enum class ItemType(val position: Int, val type: String) { + UNREAD(1, "unread"), + ALL(2, "all"), + STARRED(3, "starred"); + + companion object { + fun fromInt(value: Int) = values().first { it.position == value } + } +} \ No newline at end of file