Compare commits

..

49 Commits

Author SHA1 Message Date
Amine Bou
a3253d5a7b
Merge pull request #32 from davidoskky/di
Initial Dependency Injection implementation
2022-08-16 20:42:48 +02:00
davide
32e7a8f19c Simplify alignment menu logic 2022-08-16 12:02:20 +02:00
davide
00ef93f0c5 Remove unused repository interface 2022-08-16 11:49:53 +02:00
davide
7fb7e45093 Simplify items appending operation 2022-08-16 11:41:43 +02:00
davide
edc705ec8e Remove unused class 2022-08-16 11:23:03 +02:00
davide
bcaa6624c9 Add missing parameter 2022-08-16 11:22:14 +02:00
davide
0556500c5d Remove unused function 2022-08-16 11:19:56 +02:00
davide
f624a35fe2 Remove unused SelfossApi interface 2022-08-16 11:18:13 +02:00
davide
38f1dbd34d Strip direct api access from background 2022-08-16 11:13:26 +02:00
davide
1fb16bcbdd Remove unused user identifier 2022-08-16 10:55:22 +02:00
davide
29619e1b2b Use an enum to represent the selected items 2022-08-15 20:42:14 +02:00
davide
bb5c521387 Remove redundant functions to fetch items 2022-08-15 15:22:32 +02:00
davide
d47cca2f5a Use an enum to represent item type 2022-08-15 15:21:33 +02:00
davide
fe59177efb Do not create a new ArrayList every time the items are accessed 2022-08-15 15:07:10 +02:00
davide
baa4b81e77 Inject dateUtils 2022-08-14 13:00:37 +02:00
davide
7f8d04618a Simplify local item changes 2022-08-14 12:52:23 +02:00
davide
1890297c9d Remove commented declaration 2022-08-14 12:40:20 +02:00
davide
22f0ff664c Append older articles fetched 2022-08-14 12:39:20 +02:00
davide
88c5c6ff4b Don't regenerate dateutils each time it's needed 2022-08-13 18:00:51 +02:00
davide
3e97ee30a1 More informative log message 2022-08-13 17:57:52 +02:00
davide
5377e6c0f8 Make items read by swiping disappear 2022-08-13 17:48:13 +02:00
davide
8c5f4268a4 Remove unused variable 2022-08-13 17:41:41 +02:00
davide
5ea75a5352 Correctly show tags in side pane 2022-08-13 17:35:50 +02:00
davide
e6c70c66fc Remove searchService from the home activity 2022-08-13 17:34:09 +02:00
davide
554b8df9d0 Remove direct api access from the home activity 2022-08-13 17:11:07 +02:00
davide
442e4bf876 Do not store articles if connectivity is available. 2022-07-27 19:15:04 +02:00
davide
b14b34441b Update api login information with user input 2022-07-25 15:20:36 +02:00
davide
da4bdd2355 Simplify boolean returns 2022-07-25 14:10:50 +02:00
davide
73e6742cee Register edits locally through the repository 2022-07-25 14:08:57 +02:00
davide
924f4da1ec Remove Selfoss Service from Home Activity
Initial implementation of the missing functions in the repository
2022-07-24 14:32:40 +02:00
davide
11eac7b141 Get badges through the repository 2022-07-24 14:14:04 +02:00
davide
77fada1b02 Home Activity, use the Repository 2022-07-24 14:02:48 +02:00
davide
0859854610 Setter for a valid selectedType 2022-07-24 14:01:50 +02:00
davide
da088ec89e Use the repository to fetch the api Version 2022-07-24 03:45:54 +02:00
davide
dbbc191704 Remove unused items variables 2022-07-24 03:38:46 +02:00
davide
e37eae8d16 Fetch items through the repository
Items are stored and filtered locally in the repository
2022-07-24 03:33:51 +02:00
davide
e3d35bd653 Implement initial Items storage in Repository 2022-07-23 19:09:43 +02:00
davide
c0137ea5e7 Accept article IDs as Int in the Repository
It's cleaner to accept ints and not strings, because the ID is internally stored as an Int
2022-07-23 17:40:27 +02:00
davide
b14a6427da Inject the Repository in the Reader Activity
Removed ApiDetailsService and SelfossApi from the activity
2022-07-23 17:36:34 +02:00
davide
12e174dacd Remove SelfossApi from SourcesActivity
All network calls to access sources go through the repository
2022-07-23 01:44:47 +02:00
davide
8898e85f02 Remove Selfoss Api from the AddSourceActivity
All network calls of AddSourceActivity are now done through the repository.
2022-07-23 01:28:19 +02:00
davide
7221f11f80 Inject repository in the Article Fragment
The Repository is now injected in the Article Fragment and the DateUtils class was modified not to rely on apiDetailsService
2022-07-21 15:19:22 +02:00
davide
9373024147 Strip Selfoss API from Items Adapters 2022-07-20 14:41:27 +02:00
davide
dc10cafb1b Use the repository for the login 2022-07-20 13:43:00 +02:00
davide
3be942a807 DI Repository setup 2022-07-20 11:04:08 +02:00
davide
bbec7745fe Implement multiplatform settings 2022-07-18 23:25:10 +02:00
davide
e4fbdce30e Remove unnecessary implementation import 2022-07-18 22:07:46 +02:00
davide
21f39d64b3 Inject the Api Details Service 2022-07-18 14:05:47 +02:00
davide
c0e7b1fa0e Initial Dependency Injection implementation 2022-07-18 01:28:56 +02:00
27 changed files with 721 additions and 740 deletions

View File

@ -170,6 +170,13 @@ dependencies {
implementation("me.relex:circleindicator:2.1.6") implementation("me.relex:circleindicator:2.1.6")
implementation("androidx.viewpager2:viewpager2:1.1.0-beta01") 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 //PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0") implementation("com.github.chrisbanes:PhotoView:2.3.0")

View File

@ -1,47 +1,36 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceManager
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.appcompat.app.AppCompatActivity
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.*
import android.widget.ArrayAdapter import androidx.appcompat.app.AppCompatActivity
import android.widget.EditText import androidx.constraintlayout.widget.ConstraintLayout
import android.widget.ProgressBar import bou.amine.apps.readerforselfossv2.android.databinding.ActivityAddSourceBinding
import android.widget.Spinner
import android.widget.TextView
import android.widget.Toast
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.ftinc.scoop.Scoop 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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 var mSpoutsValue: String? = null
private lateinit var api: SelfossApi
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var binding: ActivityAddSourceBinding private lateinit var binding: ActivityAddSourceBinding
override val di by closestDI()
private val repository : Repository by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@AddSourceActivity) appColors = AppColors(this@AddSourceActivity)
@ -76,45 +65,32 @@ class AddSourceActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(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) maybeGetDetailsFromIntentSharing(intent, binding.sourceUri, binding.nameInput)
binding.saveBtn.setTextColor(appColors.colorAccent) binding.saveBtn.setTextColor(appColors.colorAccent)
binding.saveBtn.setOnClickListener { 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() { override fun onResume() {
super.onResume() super.onResume()
val config = Config(this) val config = Config()
if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) { if (config.baseUrl.isEmpty() || !config.baseUrl.isBaseUrlValid(this@AddSourceActivity)) {
mustLoginToAddSource() mustLoginToAddSource()
} else { } else {
handleSpoutsSpinner(binding.spoutsSpinner, api, binding.progress, binding.formContainer) handleSpoutsSpinner(binding.spoutsSpinner, binding.progress, binding.formContainer)
} }
} }
private fun handleSpoutsSpinner( private fun handleSpoutsSpinner(
spoutsSpinner: Spinner, spoutsSpinner: Spinner,
api: SelfossApi?,
mProgress: ProgressBar, mProgress: ProgressBar,
formContainer: ConstraintLayout formContainer: ConstraintLayout
) { ) {
@ -134,7 +110,7 @@ class AddSourceActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
var items = api!!.spouts() val items = repository.getSpouts()
if (items != null) { if (items != null) {
val itemsStrings = items.map { it.value.name } val itemsStrings = items.map { it.value.name }
@ -182,7 +158,7 @@ class AddSourceActivity : AppCompatActivity() {
finish() finish()
} }
private fun handleSaveSource(tags: EditText, title: String, url: String, api: SelfossApi) { private fun handleSaveSource(tags: EditText, title: String, url: String) {
val sourceDetailsUnavailable = val sourceDetailsUnavailable =
title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty() title.isEmpty() || url.isEmpty() || mSpoutsValue == null || mSpoutsValue!!.isEmpty()
@ -193,15 +169,14 @@ class AddSourceActivity : AppCompatActivity() {
} }
else -> { else -> {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response: SelfossModel.SuccessResponse? = api.createSourceForVersion( val successfullyAddedSource = repository.createSource(
title, title,
url, url,
mSpoutsValue!!, mSpoutsValue!!,
tags.text.toString(), tags.text.toString(),
"", "",
PreferenceManager.getDefaultSharedPreferences(this@AddSourceActivity).getInt("apiVersionMajor", 0) )
) if (successfullyAddedSource) {
if (response != null) {
finish() finish()
} else { } else {
Toast.makeText( Toast.makeText(

View File

@ -1,14 +1,11 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceManager
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View 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.AndroidDeviceDatabaseService
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity import bou.amine.apps.readerforselfossv2.android.settings.SettingsActivity
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
@ -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.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity import bou.amine.apps.readerforselfossv2.android.utils.persistence.toEntity
import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView import bou.amine.apps.readerforselfossv2.android.utils.persistence.toView
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.service.SelfossService
import bou.amine.apps.readerforselfossv2.utils.longHash import bou.amine.apps.readerforselfossv2.utils.longHash
import com.ashokvarma.bottomnavigation.BottomNavigationBar import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem import com.ashokvarma.bottomnavigation.BottomNavigationItem
@ -80,30 +71,27 @@ import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.util.addStickyFooterItem import com.mikepenz.materialdrawer.util.addStickyFooterItem
import com.mikepenz.materialdrawer.util.updateBadge import com.mikepenz.materialdrawer.util.updateBadge
import com.mikepenz.materialdrawer.widget.AccountHeaderView import com.mikepenz.materialdrawer.widget.AccountHeaderView
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
import org.kodein.di.instance
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread import kotlin.concurrent.thread
class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener { class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAware {
private lateinit var dataBase: AndroidDeviceDatabase private lateinit var dataBase: AndroidDeviceDatabase
private lateinit var dbService: AndroidDeviceDatabaseService private lateinit var dbService: AndroidDeviceDatabaseService
private lateinit var searchService: SearchService
private lateinit var apiDetailsService: ApiDetailsService
private lateinit var service: SelfossService<AndroidItemEntity>
private val MENU_PREFERENCES = 12302 private val MENU_PREFERENCES = 12302
private val DRAWER_ID_TAGS = 100101L private val DRAWER_ID_TAGS = 100101L
private val DRAWER_ID_HIDDEN_TAGS = 101100L private val DRAWER_ID_HIDDEN_TAGS = 101100L
private val DRAWER_ID_SOURCES = 100110L private val DRAWER_ID_SOURCES = 100110L
private val DRAWER_ID_FILTERS = 100111L 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<SelfossModel.Item> = ArrayList() private var items: ArrayList<SelfossModel.Item> = ArrayList()
private var allItems: ArrayList<SelfossModel.Item> = ArrayList()
private var internalBrowser = false private var internalBrowser = false
private var articleViewer = false private var articleViewer = false
@ -112,15 +100,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private var displayAllCount = false private var displayAllCount = false
private var fullHeightCards: Boolean = false private var fullHeightCards: Boolean = false
private var itemsNumber: Int = 200 private var itemsNumber: Int = 200
private var elementsShown: Int = 1 private var elementsShown: ItemType = ItemType.UNREAD
private var userIdentifier: String = ""
private var displayAccountHeader: Boolean = false private var displayAccountHeader: Boolean = false
private var infiniteScroll: Boolean = false private var infiniteScroll: Boolean = false
private var lastFetchDone: Boolean = false private var lastFetchDone: Boolean = false
private var updateSources: Boolean = true private var updateSources: Boolean = true
private var markOnScroll: Boolean = false private var markOnScroll: Boolean = false
private var hiddenTags: List<String> = emptyList() private var hiddenTags: List<String> = emptyList()
private var apiVersionMajor: Int = 0
private var periodicRefresh = false private var periodicRefresh = false
private var refreshMinutes: Long = 360L private var refreshMinutes: Long = 360L
@ -129,15 +115,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private lateinit var tabNewBadge: TextBadgeItem private lateinit var tabNewBadge: TextBadgeItem
private lateinit var tabArchiveBadge: TextBadgeItem private lateinit var tabArchiveBadge: TextBadgeItem
private lateinit var tabStarredBadge: TextBadgeItem private lateinit var tabStarredBadge: TextBadgeItem
private lateinit var api: SelfossApi
private lateinit var customTabActivityHelper: CustomTabActivityHelper private lateinit var customTabActivityHelper: CustomTabActivityHelper
private lateinit var editor: SharedPreferences.Editor
private lateinit var sharedPref: SharedPreferences
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private var offset: Int = 0 private var offset: Int = 0
private var firstVisible: Int = 0 private var firstVisible: Int = 0
private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener private lateinit var recyclerViewScrollListener: RecyclerView.OnScrollListener
private lateinit var settings: SharedPreferences private var settings = Settings()
private lateinit var binding: ActivityHomeBinding private lateinit var binding: ActivityHomeBinding
private var recyclerAdapter: RecyclerView.Adapter<*>? = null private var recyclerAdapter: RecyclerView.Adapter<*>? = null
@ -151,6 +134,9 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private lateinit var config: Config private lateinit var config: Config
override val di by closestDI()
private val repository : Repository by instance()
data class DrawerData(val tags: List<SelfossModel.Tag>?, val sources: List<SelfossModel.Source>?) data class DrawerData(val tags: List<SelfossModel.Tag>?, val sources: List<SelfossModel.Source>?)
override fun onStart() { override fun onStart() {
@ -160,7 +146,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@HomeActivity) appColors = AppColors(this@HomeActivity)
config = Config(this@HomeActivity) config = Config()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityHomeBinding.inflate(layoutInflater) binding = ActivityHomeBinding.inflate(layoutInflater)
@ -170,7 +156,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
offlineShortcut = intent.getBooleanExtra("startOffline", false) offlineShortcut = intent.getBooleanExtra("startOffline", false)
if (fromTabShortcut) { if (fromTabShortcut) {
elementsShown = intent.getIntExtra("shortcutTab", UNREAD_SHOWN) elementsShown = ItemType.fromInt(intent.getIntExtra("shortcutTab", ItemType.UNREAD.position))
} }
setContentView(view) setContentView(view)
@ -192,33 +178,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
customTabActivityHelper = CustomTabActivityHelper() 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) dataBase = AndroidDeviceDatabase(applicationContext)
searchService = SearchService(DateUtils(apiDetailsService))
dbService = AndroidDeviceDatabaseService(dataBase, searchService)
service = SelfossService(api, dbService, searchService)
items = ArrayList()
allItems = ArrayList()
handleBottomBar() handleBottomBar()
handleDrawer() handleDrawer()
handleSwipeRefreshLayout() handleSwipeRefreshLayout()
handleSharedPrefs() handleSettings()
getApiMajorVersion()
getElementsAccordingToTab() getElementsAccordingToTab()
} }
@ -231,7 +198,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
) )
binding.swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
offlineShortcut = false offlineShortcut = false
allItems = ArrayList()
lastFetchDone = false lastFetchDone = false
handleDrawerItems() handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
@ -249,7 +215,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder
): Int = ): Int =
if (elementsShown == FAV_SHOWN) { if (elementsShown == ItemType.STARRED) {
0 0
} else { } else {
super.getSwipeDirs( super.getSwipeDirs(
@ -346,17 +312,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
binding.bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC) binding.bottomBar.setBackgroundStyle(BottomNavigationBar.BACKGROUND_STYLE_STATIC)
if (fromTabShortcut) { if (fromTabShortcut) {
binding.bottomBar.selectTab(elementsShown - 1) binding.bottomBar.selectTab(elementsShown.position - 1)
}
}
private fun getApiMajorVersion() {
CoroutineScope(Dispatchers.IO).launch {
val version = api.version()
if (version != null) {
apiVersionMajor = version.getApiMajorVersion()
sharedPref.edit().putInt("apiVersionMajor", apiVersionMajor).apply()
}
} }
} }
@ -366,10 +322,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
// TODO: Make this the only appcolors init // TODO: Make this the only appcolors init
appColors = AppColors(this@HomeActivity) appColors = AppColors(this@HomeActivity)
sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
editor = settings.edit()
handleDrawerItems() handleDrawerItems()
handleThemeUpdate() handleThemeUpdate()
@ -396,34 +348,30 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
customTabActivityHelper.unbindCustomTabsService(this) customTabActivityHelper.unbindCustomTabsService(this)
} }
private fun handleSharedPrefs() { private fun handleSettings() {
internalBrowser = sharedPref.getBoolean("prefer_internal_browser", true) internalBrowser = settings.getBoolean("prefer_internal_browser", true)
articleViewer = sharedPref.getBoolean("prefer_article_viewer", true) articleViewer = settings.getBoolean("prefer_article_viewer", true)
shouldBeCardView = sharedPref.getBoolean("card_view_active", false) shouldBeCardView = settings.getBoolean("card_view_active", false)
displayUnreadCount = sharedPref.getBoolean("display_unread_count", true) displayUnreadCount = settings.getBoolean("display_unread_count", true)
displayAllCount = sharedPref.getBoolean("display_other_count", false) displayAllCount = settings.getBoolean("display_other_count", false)
fullHeightCards = sharedPref.getBoolean("full_height_cards", false) fullHeightCards = settings.getBoolean("full_height_cards", false)
itemsNumber = sharedPref.getString("prefer_api_items_number", "200")!!.toInt() itemsNumber = settings.getString("prefer_api_items_number", "200").toInt()
userIdentifier = sharedPref.getString("unique_id", "")!! displayAccountHeader = settings.getBoolean("account_header_displaying", false)
displayAccountHeader = sharedPref.getBoolean("account_header_displaying", false) infiniteScroll = settings.getBoolean("infinite_loading", false)
infiniteScroll = sharedPref.getBoolean("infinite_loading", false) updateSources = settings.getBoolean("update_sources", true)
searchService.itemsCaching = sharedPref.getBoolean("items_caching", false) markOnScroll = settings.getBoolean("mark_on_scroll", false)
updateSources = sharedPref.getBoolean("update_sources", true) hiddenTags = if (settings.getString("hidden_tags", "").isNotEmpty()) {
markOnScroll = sharedPref.getBoolean("mark_on_scroll", false) settings.getString("hidden_tags", "").replace("\\s".toRegex(), "").split(",")
hiddenTags = if (sharedPref.getString("hidden_tags", "")!!.isNotEmpty()) {
sharedPref.getString("hidden_tags", "")!!.replace("\\s".toRegex(), "").split(",")
} else { } else {
emptyList() emptyList()
} }
periodicRefresh = sharedPref.getBoolean("periodic_refresh", false) periodicRefresh = settings.getBoolean("periodic_refresh", false)
refreshWhenChargingOnly = sharedPref.getBoolean("refresh_when_charging", false) refreshWhenChargingOnly = settings.getBoolean("refresh_when_charging", false)
refreshMinutes = sharedPref.getString("periodic_refresh_minutes", "360")!!.toLong() refreshMinutes = settings.getString("periodic_refresh_minutes", "360").toLong()
if (refreshMinutes <= 15) { if (refreshMinutes <= 15) {
refreshMinutes = 15 refreshMinutes = 15
} }
apiVersionMajor = sharedPref.getInt("apiVersionMajor", 0)
} }
private fun handleThemeBinding() { private fun handleThemeBinding() {
@ -478,8 +426,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
binding.drawerContainer.addDrawerListener(drawerListener) binding.drawerContainer.addDrawerListener(drawerListener)
displayAccountHeader = displayAccountHeader =
PreferenceManager.getDefaultSharedPreferences(this) settings.getBoolean("account_header_displaying", false)
.getBoolean("account_header_displaying", false)
binding.mainDrawer.addStickyFooterItem( binding.mainDrawer.addStickyFooterItem(
PrimaryDrawerItem().apply { PrimaryDrawerItem().apply {
@ -509,7 +456,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
attachToSliderView(binding.mainDrawer) attachToSliderView(binding.mainDrawer)
addProfiles( addProfiles(
ProfileDrawerItem().apply { ProfileDrawerItem().apply {
nameText = settings.getString("url", "").toString() nameText = settings.getString("url", "")
setBackgroundResource(R.drawable.bg) setBackgroundResource(R.drawable.bg)
iconRes = R.mipmap.ic_launcher iconRes = R.mipmap.ic_launcher
selectionListEnabledForSingleProfile = false selectionListEnabledForSingleProfile = false
@ -556,10 +503,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
textColor = ColorHolder.fromColor(Color.WHITE) textColor = ColorHolder.fromColor(Color.WHITE)
color = ColorHolder.fromColor(appColors.colorAccent) } color = ColorHolder.fromColor(appColors.colorAccent) }
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() repository.tagFilter = it
searchService.tagFilter = it.tag repository.sourceFilter = null
searchService.sourceFilter = null
searchService.sourceIDFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
false false
@ -609,10 +554,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
textColor = ColorHolder.fromColor(Color.WHITE) textColor = ColorHolder.fromColor(Color.WHITE)
color = ColorHolder.fromColor(appColors.colorAccent) } color = ColorHolder.fromColor(appColors.colorAccent) }
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() repository.tagFilter = it
searchService.tagFilter = it.tag repository.sourceFilter = null
searchService.sourceFilter = null
searchService.sourceIDFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
false false
@ -643,12 +586,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
val item = PrimaryDrawerItem().apply { val item = PrimaryDrawerItem().apply {
nameText = source.getTitleDecoded() nameText = source.getTitleDecoded()
identifier = source.id.toLong() identifier = source.id.toLong()
iconUrl = source.getIcon(apiDetailsService.getBaseUrl()) iconUrl = source.getIcon(repository.baseUrl)
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() repository.sourceFilter = source
searchService.sourceIDFilter = source.id.toLong() repository.tagFilter = null
searchService.sourceFilter = source.title
searchService.tagFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
false false
@ -668,10 +609,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
identifier = DRAWER_ID_FILTERS identifier = DRAWER_ID_FILTERS
badgeRes = R.string.drawer_action_clear badgeRes = R.string.drawer_action_clear
onDrawerItemClickListener = { _,_,_ -> onDrawerItemClickListener = { _,_,_ ->
allItems = ArrayList() repository.sourceFilter = null
searchService.sourceFilter = null repository.tagFilter = null
searchService.sourceIDFilter = null
searchService.tagFilter = null
binding.mainDrawer.setSelectionAtPosition(-1) binding.mainDrawer.setSelectionAtPosition(-1)
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
@ -772,7 +711,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
fun sourcesApiCall() { fun sourcesApiCall() {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = api.sources() val response = repository.getSources()
if (response != null) { if (response != null) {
sources = response sources = response
val apiDrawerData = DrawerData(tags, sources) val apiDrawerData = DrawerData(tags, sources)
@ -791,10 +730,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut) && updateSources) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val response = api.tags() tags = repository.getTags()
if (response != null) {
tags = response
}
sourcesApiCall() sourcesApiCall()
} }
} }
@ -889,7 +825,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
offset = 0 offset = 0
lastFetchDone = false lastFetchDone = false
elementsShown = position + 1 elementsShown = ItemType.fromInt(position + 1)
getElementsAccordingToTab() getElementsAccordingToTab()
binding.recyclerView.scrollToPosition(0) binding.recyclerView.scrollToPosition(0)
@ -942,15 +878,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun getElementsAccordingToTab( private fun getElementsAccordingToTab(
appendResults: Boolean = false 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) { offset = if (appendResults && items.size > 0) {
items.size - 1 items.size - 1
} else { } else {
@ -958,45 +885,17 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
firstVisible = if (appendResults) firstVisible else 0 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 { CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
val apiItems = service.getUnreadItems(itemsNumber, offset, applicationContext.isNetworkAvailable()) repository.displayedItems = itemType
if (appendResults) { items = if (appendResults) {
apiItems?.let { items.addAll(it) } repository.getOlderItems()
} else { } else {
items = apiItems.orEmpty() as ArrayList<SelfossModel.Item> repository.getNewerItems()
}
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<SelfossModel.Item>
}
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<SelfossModel.Item>
} }
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
handleListResult() handleListResult()
@ -1021,17 +920,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
ItemCardAdapter( ItemCardAdapter(
this, this,
items, items,
api,
apiDetailsService,
db, db,
customTabActivityHelper, customTabActivityHelper,
internalBrowser, internalBrowser,
articleViewer, articleViewer,
fullHeightCards, fullHeightCards,
appColors, appColors,
userIdentifier, config
config,
searchService
) { ) {
updateItems(it) updateItems(it)
} }
@ -1040,16 +935,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
ItemListAdapter( ItemListAdapter(
this, this,
items, items,
api,
apiDetailsService,
db, db,
customTabActivityHelper, customTabActivityHelper,
internalBrowser, internalBrowser,
articleViewer, articleViewer,
userIdentifier,
appColors, appColors,
config, config
searchService
) { ) {
updateItems(it) updateItems(it)
} }
@ -1073,8 +964,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun reloadBadges() { private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) { if (displayUnreadCount || displayAllCount) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
service.reloadBadges(applicationContext.isNetworkAvailable()) if (applicationContext.isNetworkAvailable()) {
reloadBadgeContent() repository.reloadBadges()
reloadBadgeContent()
}
} }
} }
} }
@ -1082,15 +975,15 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun reloadBadgeContent() { private fun reloadBadgeContent() {
if (displayUnreadCount) { if (displayUnreadCount) {
tabNewBadge tabNewBadge
.setText(searchService.badgeUnread.toString()) .setText(repository.badgeUnread.toString())
.maybeShow() .maybeShow()
} }
if (displayAllCount) { if (displayAllCount) {
tabArchiveBadge tabArchiveBadge
.setText(searchService.badgeAll.toString()) .setText(repository.badgeAll.toString())
.maybeShow() .maybeShow()
tabStarredBadge tabStarredBadge
.setText(searchService.badgeStarred.toString()) .setText(repository.badgeStarred.toString())
.maybeShow() .maybeShow()
} }
} }
@ -1110,7 +1003,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
override fun onQueryTextChange(p0: String?): Boolean { override fun onQueryTextChange(p0: String?): Boolean {
if (p0.isNullOrBlank()) { if (p0.isNullOrBlank()) {
searchService.searchFilter = null repository.searchFilter = null
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
} }
@ -1118,7 +1011,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
override fun onQueryTextSubmit(p0: String?): Boolean { override fun onQueryTextSubmit(p0: String?): Boolean {
searchService.searchFilter = p0 repository.searchFilter = p0
getElementsAccordingToTab() getElementsAccordingToTab()
fetchOnEmptyList() fetchOnEmptyList()
return false return false
@ -1151,9 +1044,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) { needsConfirmation(R.string.menu_home_refresh, R.string.refresh_dialog_message) {
Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.refresh_in_progress, Toast.LENGTH_SHORT).show()
// TODO: Use Dispatchers.IO
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val status = api.update() val updatedRemote = repository.updateRemote()
if (status != null && status.isSuccess) { if (updatedRemote) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG R.string.refresh_success_response, Toast.LENGTH_LONG
@ -1174,13 +1068,13 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
} }
R.id.readAll -> { R.id.readAll -> {
if (elementsShown == UNREAD_SHOWN) { if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) { needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true binding.swipeRefreshLayout.isRefreshing = true
if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) { if (this@HomeActivity.isNetworkAvailable(null, offlineShortcut)) {
CoroutineScope(Dispatchers.Main).launch { 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) { if (success) {
Toast.makeText( Toast.makeText(
this@HomeActivity, this@HomeActivity,
@ -1208,7 +1102,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
return true return true
} }
R.id.action_disconnect -> { R.id.action_disconnect -> {
return Config.logoutAndRedirect(this, this@HomeActivity, editor) return Config.logoutAndRedirect(this, this@HomeActivity)
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }
@ -1216,10 +1110,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
private fun maxItemNumber(): Int = private fun maxItemNumber(): Int =
when (elementsShown) { when (elementsShown) {
UNREAD_SHOWN -> searchService.badgeUnread ItemType.UNREAD -> repository.badgeUnread
READ_SHOWN -> searchService.badgeAll ItemType.ALL -> repository.badgeAll
FAV_SHOWN -> searchService.badgeStarred ItemType.STARRED -> repository.badgeStarred
else -> searchService.badgeUnread // if !elementsShown then unread are fetched. else -> repository.badgeUnread // if !elementsShown then unread are fetched.
} }
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) { private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
@ -1245,8 +1139,8 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
} }
private fun handleOfflineActions() { private fun handleOfflineActions() {
fun doAndReportOnFail(call: SelfossModel.SuccessResponse?, action: ActionEntity) { fun doAndReportOnFail(success: Boolean, action: ActionEntity) {
if (call != null && call.isSuccess) { if (success) {
thread { thread {
db.actionsDao().delete(action) db.actionsDao().delete(action)
} }
@ -1259,10 +1153,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener {
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail(api.markAsRead(action.articleId), action) action.read -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
action.unread -> doAndReportOnFail(api.unmarkAsRead(action.articleId), action) action.unread -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
action.starred -> doAndReportOnFail(api.starr(action.articleId), action) action.starred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
action.unstarred -> doAndReportOnFail(api.unstarr(action.articleId), action) action.unstarred -> doAndReportOnFail(repository.markAsRead(action.articleId.toInt()), action)
} }
} }
} }

View File

@ -3,48 +3,43 @@ package bou.amine.apps.readerforselfossv2.android
import android.animation.Animator import android.animation.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import android.text.TextUtils import android.text.TextUtils
import android.util.Log
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView import android.widget.TextView
import androidx.preference.PreferenceManager import androidx.appcompat.app.AlertDialog
import androidx.work.Logger import androidx.appcompat.app.AppCompatActivity
import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivityLoginBinding
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid import bou.amine.apps.readerforselfossv2.android.utils.isBaseUrlValid
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable 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.service.ApiDetailsService
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Call import org.kodein.di.DIAware
import retrofit2.Callback import org.kodein.di.android.closestDI
import retrofit2.Response import org.kodein.di.instance
class LoginActivity : AppCompatActivity() { class LoginActivity() : AppCompatActivity(), DIAware {
private var inValidCount: Int = 0 private var inValidCount: Int = 0
private var isWithSelfSignedCert = false private var isWithSelfSignedCert = false
private var isWithLogin = false private var isWithLogin = false
private var isWithHTTPLogin = false private var isWithHTTPLogin = false
private lateinit var settings: SharedPreferences private val settings = Settings()
private lateinit var editor: SharedPreferences.Editor
private lateinit var userIdentifier: String
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var binding: ActivityLoginBinding private lateinit var binding: ActivityLoginBinding
override val di by closestDI()
private val repository : Repository by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@LoginActivity) appColors = AppColors(this@LoginActivity)
@ -58,12 +53,7 @@ class LoginActivity : AppCompatActivity() {
handleBaseUrlFail() handleBaseUrlFail()
settings = PreferenceManager.getDefaultSharedPreferences(applicationContext) if (settings.getString("url", "").isNotEmpty()) {
userIdentifier = settings.getString("unique_id", "")!!
editor = settings.edit()
if (settings.getString("url", "")!!.isNotEmpty()) {
goToMain() goToMain()
} }
@ -128,18 +118,16 @@ class LoginActivity : AppCompatActivity() {
} }
private fun preferenceError(t: Throwable) { private fun preferenceError(t: Throwable) {
editor.remove("url") settings.remove("url")
editor.remove("login") settings.remove("login")
editor.remove("httpUserName") settings.remove("httpUserName")
editor.remove("password") settings.remove("password")
editor.remove("httpPassword") settings.remove("httpPassword")
editor.apply()
binding.urlView.error = getString(R.string.wrong_infos) binding.urlView.error = getString(R.string.wrong_infos)
binding.loginView.error = getString(R.string.wrong_infos) binding.loginView.error = getString(R.string.wrong_infos)
binding.passwordView.error = getString(R.string.wrong_infos) binding.passwordView.error = getString(R.string.wrong_infos)
binding.httpLoginView.error = getString(R.string.wrong_infos) binding.httpLoginView.error = getString(R.string.wrong_infos)
binding.httpPasswordView.error = getString(R.string.wrong_infos) binding.httpPasswordView.error = getString(R.string.wrong_infos)
showProgress(false)
} }
private fun attemptLogin() { private fun attemptLogin() {
@ -212,39 +200,21 @@ class LoginActivity : AppCompatActivity() {
} else { } else {
showProgress(true) showProgress(true)
editor.putString("url", url) repository.refreshLoginInformation(url, login, password, httpLogin, httpPassword, isWithSelfSignedCert)
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
)
if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) { if (this@LoginActivity.isNetworkAvailable(this@LoginActivity.findViewById(R.id.loginForm))) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { val result = repository.login()
val result = api.login() if (result) {
if (result != null && result.isSuccess) { goToMain()
goToMain() } else {
} else { CoroutineScope(Dispatchers.Main).launch {
preferenceError(Exception("Not success")) preferenceError(Exception("Not success"))
} }
} catch (cause: Throwable) {
Log.e("1", "LOL")
} }
} }
} else {
showProgress(false)
} }
showProgress(false)
} }
} }

View File

@ -6,31 +6,35 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.preference.PreferenceManager
import android.widget.ImageView import android.widget.ImageView
import androidx.multidex.MultiDexApplication 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.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth 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.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader 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<Repository>() with singleton { Repository(instance(), instance()) }
}
class MyApp : MultiDexApplication() {
private lateinit var config: Config private lateinit var config: Config
private lateinit var settings : Settings
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
config = Config(baseContext) config = Config()
settings = Settings()
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()
}
initDrawerImageLoader() initDrawerImageLoader()

View File

@ -1,7 +1,5 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
@ -10,7 +8,6 @@ import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import androidx.room.Room import androidx.room.Room
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
@ -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_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.toggleStar 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.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import 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 markOnScroll: Boolean = false
private var currentItem: Int = 0 private var currentItem: Int = 0
private lateinit var userIdentifier: String
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var api: SelfossApi
private lateinit var toolbarMenu: Menu private lateinit var toolbarMenu: Menu
private lateinit var db: AppDatabase private lateinit var db: AppDatabase
private lateinit var prefs: SharedPreferences
private lateinit var binding: ActivityReaderBinding private lateinit var binding: ActivityReaderBinding
private var activeAlignment: Int = 1 private var activeAlignment: Int = 1
private val JUSTIFY = 1 private val JUSTIFY = 1
private val ALIGN_LEFT = 2 private val ALIGN_LEFT = 2
override val di by closestDI()
private val repository : Repository by instance()
private fun showMenuItem(willAddToFavorite: Boolean) { private fun showMenuItem(willAddToFavorite: Boolean) {
if (willAddToFavorite) { if (willAddToFavorite) {
toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE) toolbarMenu.findItem(R.id.star).icon.setTint(Color.WHITE)
@ -68,7 +65,7 @@ class ReaderActivity : AppCompatActivity() {
showMenuItem(false) showMenuItem(false)
} }
private lateinit var editor: SharedPreferences.Editor private var settings = Settings()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -91,23 +88,8 @@ class ReaderActivity : AppCompatActivity() {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
val settings = markOnScroll = settings.getBoolean("mark_on_scroll", false)
getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) activeAlignment = settings.getInt("text_align", JUSTIFY)
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)
)
if (allItems.isEmpty()) { if (allItems.isEmpty()) {
finish() finish()
@ -130,8 +112,8 @@ class ReaderActivity : AppCompatActivity() {
private fun readItem(item: SelfossModel.Item) { private fun readItem(item: SelfossModel.Item) {
if (markOnScroll) { if (markOnScroll) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.markAsRead(item.id.toString()) repository.markAsRead(item.id)
// TODO: update item in DB // 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_left).isVisible = !showJustify
toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify toolbarMenu.findItem(R.id.align_justify).isVisible = showJustify
} }
@ -183,11 +166,7 @@ class ReaderActivity : AppCompatActivity() {
} else { } else {
canFavorite() canFavorite()
} }
if (activeAlignment == JUSTIFY) { alignmentMenu()
alignmentMenu(false)
} else {
alignmentMenu(true)
}
binding.pager.registerOnPageChangeCallback( binding.pager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
@ -228,34 +207,37 @@ class ReaderActivity : AppCompatActivity() {
R.id.star -> { R.id.star -> {
if (allItems[binding.pager.currentItem].starred) { if (allItems[binding.pager.currentItem].starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.unstarr(allItems[binding.pager.currentItem].id.toString()) repository.unstarr(allItems[binding.pager.currentItem].id)
// TODO: update in DB // TODO: Handle failure
} }
afterUnsave() afterUnsave()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.starr(allItems[binding.pager.currentItem].id.toString()) repository.starr(allItems[binding.pager.currentItem].id)
// TODO: update in DB // TODO: Handle failure
} }
afterSave() afterSave()
} }
} }
R.id.align_left -> { R.id.align_left -> {
editor.putInt("text_align", ALIGN_LEFT) activeAlignment = ALIGN_LEFT
editor.apply() switchAlignmentSetting()
alignmentMenu(true)
refreshFragment() refreshFragment()
} }
R.id.align_justify -> { R.id.align_justify -> {
editor.putInt("text_align", JUSTIFY) activeAlignment = JUSTIFY
editor.apply() switchAlignmentSetting()
alignmentMenu(false)
refreshFragment() refreshFragment()
} }
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
private fun switchAlignmentSetting() {
settings.putInt("text_align", activeAlignment)
alignmentMenu()
}
private fun refreshFragment() { private fun refreshFragment() {
finish() finish()
overridePendingTransition(0, 0) overridePendingTransition(0, 0)

View File

@ -1,37 +1,34 @@
package bou.amine.apps.readerforselfossv2.android package bou.amine.apps.readerforselfossv2.android
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceManager import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import android.widget.Toast
import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter import bou.amine.apps.readerforselfossv2.android.adapters.SourcesListAdapter
import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding import bou.amine.apps.readerforselfossv2.android.databinding.ActivitySourcesBinding
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.themes.Toppings import bou.amine.apps.readerforselfossv2.android.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable 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.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Call import org.kodein.di.DIAware
import retrofit2.Callback import org.kodein.di.android.closestDI
import retrofit2.Response import org.kodein.di.instance
import java.util.ArrayList
class SourcesActivity : AppCompatActivity() { class SourcesActivity : AppCompatActivity(), DIAware {
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var binding: ActivitySourcesBinding private lateinit var binding: ActivitySourcesBinding
override val di by closestDI()
private val repository : Repository by instance()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(this@SourcesActivity) appColors = AppColors(this@SourcesActivity)
binding = ActivitySourcesBinding.inflate(layoutInflater) binding = ActivitySourcesBinding.inflate(layoutInflater)
@ -62,18 +59,6 @@ class SourcesActivity : AppCompatActivity() {
super.onResume() super.onResume()
val mLayoutManager = LinearLayoutManager(this) 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<SelfossModel.Source> var items: ArrayList<SelfossModel.Source>
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
@ -81,11 +66,11 @@ class SourcesActivity : AppCompatActivity() {
if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) { if (this@SourcesActivity.isNetworkAvailable(binding.recyclerView)) {
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val response = api.sources() val response = repository.getSources()
if (response != null) { if (response != null) {
items = response items = response
val mAdapter = SourcesListAdapter(this@SourcesActivity, items, api, val mAdapter = SourcesListAdapter(
apiDetailsService this@SourcesActivity, items
) )
binding.recyclerView.adapter = mAdapter binding.recyclerView.adapter = mAdapter
mAdapter.notifyDataSetChanged() mAdapter.notifyDataSetChanged()

View File

@ -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.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel 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.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DI
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class ItemCardAdapter( class ItemCardAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override var items: ArrayList<SelfossModel.Item>,
override val api: SelfossApi,
override val apiDetailsService: ApiDetailsService,
override val db: AppDatabase, override val db: AppDatabase,
private val helper: CustomTabActivityHelper, private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean, private val internalBrowser: Boolean,
private val articleViewer: Boolean, private val articleViewer: Boolean,
private val fullHeightCards: Boolean, private val fullHeightCards: Boolean,
override val appColors: AppColors, override val appColors: AppColors,
override val userIdentifier: String,
override val config: Config, override val config: Config,
override val searchService: SearchService,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemCardAdapter.ViewHolder>() { ) : ItemsAdapter<ItemCardAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
@ -50,6 +46,9 @@ class ItemCardAdapter(
private val imageMaxHeight: Int = private val imageMaxHeight: Int =
c.resources.getDimension(R.dimen.card_image_max_height).toInt() 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = CardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) return ViewHolder(binding)
@ -66,23 +65,23 @@ class ItemCardAdapter(
binding.title.setLinkTextColor(appColors.colorAccent) binding.title.setLinkTextColor(appColors.colorAccent)
binding.sourceTitleAndDate.text = itm.sourceAndDateText(DateUtils(apiDetailsService)) binding.sourceTitleAndDate.text = itm.sourceAndDateText(repository.dateUtils)
if (!fullHeightCards) { if (!fullHeightCards) {
binding.itemImage.maxHeight = imageMaxHeight binding.itemImage.maxHeight = imageMaxHeight
binding.itemImage.scaleType = ScaleType.CENTER_CROP binding.itemImage.scaleType = ScaleType.CENTER_CROP
} }
if (itm.getThumbnail(apiDetailsService.getBaseUrl()).isEmpty()) { if (itm.getThumbnail(repository.baseUrl).isEmpty()) {
binding.itemImage.visibility = View.GONE binding.itemImage.visibility = View.GONE
Glide.with(c).clear(binding.itemImage) Glide.with(c).clear(binding.itemImage)
binding.itemImage.setImageDrawable(null) binding.itemImage.setImageDrawable(null)
} else { } else {
binding.itemImage.visibility = View.VISIBLE binding.itemImage.visibility = View.VISIBLE
c.bitmapCenterCrop(config, itm.getThumbnail(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 color = generator.getColor(itm.getSourceTitle())
val drawable = val drawable =
@ -92,7 +91,7 @@ class ItemCardAdapter(
.build(itm.getSourceTitle().toTextDrawableString(c), color) .build(itm.getSourceTitle().toTextDrawableString(c), color)
binding.sourceImage.setImageDrawable(drawable) binding.sourceImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.sourceImage) c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.sourceImage)
} }
} }
} }
@ -114,15 +113,15 @@ class ItemCardAdapter(
if (c.isNetworkAvailable()) { if (c.isNetworkAvailable()) {
if (item.starred) { if (item.starred) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.unstarr(item.id.toString()) repository.unstarr(item.id)
// TODO: save to db // TODO: Handle failure
} }
item.starred = false item.starred = false
binding.favButton.isSelected = false binding.favButton.isSelected = false
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.starr(item.id.toString()) repository.starr(item.id)
// TODO: save to db // TODO: Handle failure
} }
item.starred = true item.starred = true
binding.favButton.isSelected = true binding.favButton.isSelected = true
@ -152,8 +151,7 @@ class ItemCardAdapter(
customTabsIntent, customTabsIntent,
internalBrowser, internalBrowser,
articleViewer, articleViewer,
app, app
searchService
) )
} }
} }

View File

@ -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.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop import bou.amine.apps.readerforselfossv2.android.utils.glide.bitmapCenterCrop
import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable import bou.amine.apps.readerforselfossv2.android.utils.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel 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.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import org.kodein.di.DI
import org.kodein.di.android.closestDI
import org.kodein.di.instance
class ItemListAdapter( class ItemListAdapter(
override val app: Activity, override val app: Activity,
override var items: ArrayList<SelfossModel.Item>, override var items: ArrayList<SelfossModel.Item>,
override val api: SelfossApi,
override val apiDetailsService: ApiDetailsService,
override val db: AppDatabase, override val db: AppDatabase,
private val helper: CustomTabActivityHelper, private val helper: CustomTabActivityHelper,
private val internalBrowser: Boolean, private val internalBrowser: Boolean,
private val articleViewer: Boolean, private val articleViewer: Boolean,
override val userIdentifier: String,
override val appColors: AppColors, override val appColors: AppColors,
override val config: Config, override val config: Config,
override val searchService: SearchService,
override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit override val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
) : ItemsAdapter<ItemListAdapter.ViewHolder>() { ) : ItemsAdapter<ItemListAdapter.ViewHolder>() {
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
private val c: Context = app.baseContext private val c: Context = app.baseContext
override val di: DI by closestDI(app)
override val repository : Repository by instance()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding) return ViewHolder(binding)
@ -54,11 +53,11 @@ class ItemListAdapter(
binding.title.setLinkTextColor(appColors.colorAccent) 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 color = generator.getColor(itm.getSourceTitle())
val drawable = val drawable =
@ -69,10 +68,10 @@ class ItemListAdapter(
binding.itemImage.setImageDrawable(drawable) binding.itemImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage) c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
} }
} else { } 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, customTabsIntent,
internalBrowser, internalBrowser,
articleViewer, articleViewer,
app, app
searchService
) )
} }
} }

View File

@ -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.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.service.SearchService
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware
abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>() { abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapter<VH>(), DIAware {
abstract var items: ArrayList<SelfossModel.Item> abstract var items: ArrayList<SelfossModel.Item>
abstract val api: SelfossApi abstract val repository: Repository
abstract val apiDetailsService: ApiDetailsService
abstract val db: AppDatabase abstract val db: AppDatabase
abstract val userIdentifier: String
abstract val app: Activity abstract val app: Activity
abstract val appColors: AppColors abstract val appColors: AppColors
abstract val config: Config abstract val config: Config
abstract val searchService: SearchService
abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit abstract val updateItems: (ArrayList<SelfossModel.Item>) -> Unit
fun updateAllItems(items: ArrayList<SelfossModel.Item>) { fun updateAllItems(items: ArrayList<SelfossModel.Item>) {
@ -82,18 +79,15 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) { private fun readItemAtIndex(position: Int, showSnackbar: Boolean = true) {
val i = items[position] val i = items[position]
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.markAsRead(i.id.toString()) repository.markAsRead(i.id)
// TODO: update db }
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) { if (showSnackbar) {
unmarkSnackbar(i, position) unmarkSnackbar(i, position)
} }
@ -101,7 +95,7 @@ abstract class ItemsAdapter<VH : RecyclerView.ViewHolder?> : RecyclerView.Adapte
private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) { private fun unreadItemAtIndex(position: Int, showSnackbar: Boolean = true) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.unmarkAsRead(items[position].id.toString()) repository.unmarkAsRead(items[position].id)
// Todo: SharedItems.unreadItem(app, api, db, items[position]) // Todo: SharedItems.unreadItem(app, api, db, items[position])
// TODO: update db // TODO: update db

View File

@ -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.glide.circularBitmapDrawable
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.android.utils.toTextDrawableString 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.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator import com.amulyakhare.textdrawable.util.ColorGenerator
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch 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( class SourcesListAdapter(
private val app: Activity, private val app: Activity,
private val items: ArrayList<SelfossModel.Source>, private val items: ArrayList<SelfossModel.Source>
private val api: SelfossApi, ) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>(), DIAware {
private val apiDetailsService: ApiDetailsService
) : RecyclerView.Adapter<SourcesListAdapter.ViewHolder>() {
private val c: Context = app.baseContext private val c: Context = app.baseContext
private val generator: ColorGenerator = ColorGenerator.MATERIAL private val generator: ColorGenerator = ColorGenerator.MATERIAL
private lateinit var config: Config private lateinit var config: Config
private lateinit var binding: SourceListItemBinding 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding = SourceListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding.root) return ViewHolder(binding.root)
@ -43,9 +47,9 @@ class SourcesListAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val itm = items[position] 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 color = generator.getColor(itm.getTitleDecoded())
val drawable = val drawable =
@ -55,7 +59,7 @@ class SourcesListAdapter(
.build(itm.getTitleDecoded().toTextDrawableString(c), color) .build(itm.getTitleDecoded().toTextDrawableString(c), color)
binding.itemImage.setImageDrawable(drawable) binding.itemImage.setImageDrawable(drawable)
} else { } else {
c.circularBitmapDrawable(config, itm.getIcon(apiDetailsService.getBaseUrl()), binding.itemImage) c.circularBitmapDrawable(config, itm.getIcon(repository.baseUrl), binding.itemImage)
} }
binding.sourceTitle.text = itm.getTitleDecoded() binding.sourceTitle.text = itm.getTitleDecoded()
@ -77,8 +81,8 @@ class SourcesListAdapter(
if (c.isNetworkAvailable(null)) { if (c.isNetworkAvailable(null)) {
val (id) = items[adapterPosition] val (id) = items[adapterPosition]
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val action = api.deleteSource(id) val successfullyDeletedSource = repository.deleteSource(id)
if (action != null && action.isSuccess) { if (successfullyDeletedSource) {
items.removeAt(adapterPosition) items.removeAt(adapterPosition)
notifyItemRemoved(adapterPosition) notifyItemRemoved(adapterPosition)
notifyItemRangeChanged(adapterPosition, itemCount) notifyItemRangeChanged(adapterPosition, itemCount)

View File

@ -5,7 +5,6 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.preference.PreferenceManager
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationCompat.PRIORITY_LOW import androidx.core.app.NotificationCompat.PRIORITY_LOW
@ -13,52 +12,39 @@ import androidx.room.Room
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import bou.amine.apps.readerforselfossv2.android.MainActivity import bou.amine.apps.readerforselfossv2.android.MainActivity
import bou.amine.apps.readerforselfossv2.android.MyApp
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.model.preloadImages 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.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity import bou.amine.apps.readerforselfossv2.android.persistence.entities.ActionEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossApi
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.SearchService import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.service.SelfossService import com.russhwolf.settings.Settings
import bou.amine.apps.readerforselfossv2.utils.DateUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.instance
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.concurrent.thread 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 lateinit var db: AppDatabase
override fun doWork(): Result { override val di by lazy { (applicationContext as MyApp).di }
val settings = private val repository : Repository by instance()
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
)
val dateUtils = DateUtils(apiDetailsService) override fun doWork(): Result {
val searchService = SearchService(dateUtils) val settings = Settings()
val service = SelfossService(api, AndroidDeviceDatabaseService(AndroidDeviceDatabase(applicationContext), searchService), searchService) val periodicRefresh = settings.getBoolean("periodic_refresh", false)
if (periodicRefresh) {
if (context.isNetworkAvailable()) { if (context.isNetworkAvailable()) {
@ -77,7 +63,7 @@ override fun doWork(): Result {
notificationManager.notify(1, notification.build()) notificationManager.notify(1, notification.build())
val notifyNewItems = sharedPref.getBoolean("notify_new_items", false) val notifyNewItems = settings.getBoolean("notify_new_items", false)
db = Room.databaseBuilder( db = Room.databaseBuilder(
applicationContext, applicationContext,
@ -90,19 +76,19 @@ override fun doWork(): Result {
actions.forEach { action -> actions.forEach { action ->
when { when {
action.read -> doAndReportOnFail( action.read -> doAndReportOnFail(
api.markAsRead(action.articleId), repository.markAsRead(action.articleId.toInt()),
action action
) )
action.unread -> doAndReportOnFail( action.unread -> doAndReportOnFail(
api.unmarkAsRead(action.articleId), repository.unmarkAsRead(action.articleId.toInt()),
action action
) )
action.starred -> doAndReportOnFail( action.starred -> doAndReportOnFail(
api.starr(action.articleId), repository.starr(action.articleId.toInt()),
action action
) )
action.unstarred -> doAndReportOnFail( action.unstarred -> doAndReportOnFail(
api.unstarr(action.articleId), repository.unstarr(action.articleId.toInt()),
action action
) )
} }
@ -111,10 +97,10 @@ override fun doWork(): Result {
if (context.isNetworkAvailable()) { if (context.isNetworkAvailable()) {
launch { launch {
try { try {
val newItems = service.allNewItems() val newItems = repository.allItems(ItemType.UNREAD)
handleNewItemsNotification(newItems, notifyNewItems, notificationManager) handleNewItemsNotification(newItems, notifyNewItems, notificationManager)
val readItems = service.allReadItems() val readItems = repository.allItems(ItemType.ALL)
val starredItems = service.allStarredItems() val starredItems = repository.allItems(ItemType.STARRED)
// TODO: save all to DB // TODO: save all to DB
} catch (e: Throwable) {} } catch (e: Throwable) {}
} }
@ -173,8 +159,9 @@ override fun doWork(): Result {
} }
} }
private fun doAndReportOnFail(result: SelfossModel.SuccessResponse?, action: ActionEntity) { private fun doAndReportOnFail(result: Boolean, action: ActionEntity) {
if (result != null && result.isSuccess) { // TODO: Failures should be reported
if (result) {
thread { thread {
db.actionsDao().delete(action) db.actionsDao().delete(action)
} }

View File

@ -1,8 +1,6 @@
package bou.amine.apps.readerforselfossv2.android.fragments package bou.amine.apps.readerforselfossv2.android.fragments
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.TypedArray import android.content.res.TypedArray
import android.graphics.Bitmap import android.graphics.Bitmap
@ -11,14 +9,16 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.* 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 android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import androidx.room.Room import androidx.room.Room
import bou.amine.apps.readerforselfossv2.android.ImageActivity import bou.amine.apps.readerforselfossv2.android.ImageActivity
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
@ -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.api.mercury.ParsedContent
import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding import bou.amine.apps.readerforselfossv2.android.databinding.FragmentArticleBinding
import bou.amine.apps.readerforselfossv2.android.model.* import bou.amine.apps.readerforselfossv2.android.model.*
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.AndroidDeviceDatabaseService
import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase import bou.amine.apps.readerforselfossv2.android.persistence.database.AppDatabase
import bou.amine.apps.readerforselfossv2.android.persistence.entities.AndroidItemEntity
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_1_2
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_2_3
import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4 import bou.amine.apps.readerforselfossv2.android.persistence.migrations.MIGRATION_3_4
import bou.amine.apps.readerforselfossv2.android.service.AndroidApiDetailsService
import bou.amine.apps.readerforselfossv2.android.themes.AppColors import bou.amine.apps.readerforselfossv2.android.themes.AppColors
import bou.amine.apps.readerforselfossv2.android.utils.* import bou.amine.apps.readerforselfossv2.android.utils.*
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.android.utils.glide.getBitmapInputStream 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.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable import bou.amine.apps.readerforselfossv2.android.utils.network.isNetworkAvailable
import bou.amine.apps.readerforselfossv2.rest.SelfossApi import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.rest.SelfossModel 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 bou.amine.apps.readerforselfossv2.utils.isEmptyOrNullOrNullString
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.github.rubensousa.floatingtoolbar.FloatingToolbar import com.github.rubensousa.floatingtoolbar.FloatingToolbar
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.russhwolf.settings.Settings
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import 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.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -63,10 +60,7 @@ import java.net.URL
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
class ArticleFragment : Fragment() { class ArticleFragment : Fragment(), DIAware {
private lateinit var dbService: AndroidDeviceDatabaseService
private lateinit var apiDetailsService: ApiDetailsService
private lateinit var service: SelfossService<AndroidItemEntity>
private var fontSize: Int = 16 private var fontSize: Int = 16
private lateinit var item: SelfossModel.Item private lateinit var item: SelfossModel.Item
private var mCustomTabActivityHelper: CustomTabActivityHelper? = null private var mCustomTabActivityHelper: CustomTabActivityHelper? = null
@ -76,7 +70,6 @@ class ArticleFragment : Fragment() {
private lateinit var contentImage: String private lateinit var contentImage: String
private lateinit var contentTitle: String private lateinit var contentTitle: String
private lateinit var allImages : ArrayList<String> private lateinit var allImages : ArrayList<String>
private lateinit var editor: SharedPreferences.Editor
private lateinit var fab: FloatingActionButton private lateinit var fab: FloatingActionButton
private lateinit var appColors: AppColors private lateinit var appColors: AppColors
private lateinit var db: AppDatabase private lateinit var db: AppDatabase
@ -85,7 +78,10 @@ class ArticleFragment : Fragment() {
private var _binding: FragmentArticleBinding? = null private var _binding: FragmentArticleBinding? = null
private val binding get() = _binding!! 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 typeface: Typeface? = null
private var resId: Int = 0 private var resId: Int = 0
@ -101,16 +97,10 @@ class ArticleFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
appColors = AppColors(requireActivity()) appColors = AppColors(requireActivity())
config = Config(requireActivity()) config = Config()
super.onCreate(savedInstanceState) 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)!! val pi: ParecelableItem = requireArguments().getParcelable(ARG_ITEMS)!!
item = pi.toModel() item = pi.toModel()
@ -132,16 +122,14 @@ class ArticleFragment : Fragment() {
url = item.getLinkDecoded() url = item.getLinkDecoded()
contentText = item.content contentText = item.content
contentTitle = item.getTitleDecoded() contentTitle = item.getTitleDecoded()
contentImage = item.getThumbnail(apiDetailsService.getBaseUrl()) contentImage = item.getThumbnail(repository.baseUrl)
contentSource = item.sourceAndDateText(DateUtils(apiDetailsService)) contentSource = item.sourceAndDateText(repository.dateUtils)
allImages = item.getImages() allImages = item.getImages()
prefs = PreferenceManager.getDefaultSharedPreferences(activity) fontSize = settings.getString("reader_font_size", "16").toInt()
editor = prefs.edit() staticBar = settings.getBoolean("reader_static_bar", false)
fontSize = prefs.getString("reader_font_size", "16")!!.toInt()
staticBar = prefs.getBoolean("reader_static_bar", false)
font = prefs.getString("reader_font", "")!! font = settings.getString("reader_font", "")
if (font.isNotEmpty()) { if (font.isNotEmpty()) {
resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName) resId = requireContext().resources.getIdentifier(font, "font", requireContext().packageName)
typeface = try { typeface = try {
@ -155,16 +143,6 @@ class ArticleFragment : Fragment() {
refreshAlignment() 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 = binding.fab
fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent) fab.backgroundTintList = ColorStateList.valueOf(appColors.colorAccent)
@ -191,8 +169,7 @@ class ArticleFragment : Fragment() {
R.id.unread_action -> if (context != null) { R.id.unread_action -> if (context != null) {
if (this@ArticleFragment.item.unread) { if (this@ArticleFragment.item.unread) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.markAsRead(this@ArticleFragment.item.id.toString()) repository.markAsRead(this@ArticleFragment.item.id)
// TODO: Update in DB
} }
this@ArticleFragment.item.unread = false this@ArticleFragment.item.unread = false
Toast.makeText( Toast.makeText(
@ -202,8 +179,7 @@ class ArticleFragment : Fragment() {
).show() ).show()
} else { } else {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
api.unmarkAsRead(this@ArticleFragment.item.id.toString()) repository.unmarkAsRead(this@ArticleFragment.item.id)
// TODO: Update in DB
} }
this@ArticleFragment.item.unread = true this@ArticleFragment.item.unread = true
Toast.makeText( Toast.makeText(
@ -276,10 +252,7 @@ class ArticleFragment : Fragment() {
.setTitle(requireContext().getString(R.string.webview_dialog_issue_title)) .setTitle(requireContext().getString(R.string.webview_dialog_issue_title))
.setPositiveButton(android.R.string.ok .setPositiveButton(android.R.string.ok
) { _, _ -> ) { _, _ ->
val sharedPref = PreferenceManager.getDefaultSharedPreferences(requireContext()) settings.putBoolean("prefer_article_viewer", false)
val editor = sharedPref.edit()
editor.putBoolean("prefer_article_viewer", false)
editor.apply()
requireActivity().finish() requireActivity().finish()
} }
.create() .create()
@ -295,7 +268,7 @@ class ArticleFragment : Fragment() {
} }
private fun refreshAlignment() { private fun refreshAlignment() {
textAlignment = when (prefs.getInt("text_align", 1)) { textAlignment = when (settings.getInt("text_align", 1)) {
1 -> "justify" 1 -> "justify"
2 -> "left" 2 -> "left"
else -> "justify" else -> "justify"

View File

@ -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.themes.Toppings
import bou.amine.apps.readerforselfossv2.android.utils.Config import bou.amine.apps.readerforselfossv2.android.utils.Config
import com.ftinc.scoop.Scoop import com.ftinc.scoop.Scoop
import com.russhwolf.settings.Settings
import java.lang.NumberFormatException import java.lang.NumberFormatException
private const val TITLE_TAG = "settingsActivityTitle" private const val TITLE_TAG = "settingsActivityTitle"
@ -173,14 +174,12 @@ class SettingsActivity : AppCompatActivity(),
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
val id = item.itemId val id = item.itemId
if (id == R.id.clear) { if (id == R.id.clear) {
val pref = PreferenceManager.getDefaultSharedPreferences(activity) val settings = Settings()
val editor = pref.edit() settings.remove("color_primary")
editor.remove("color_primary") settings.remove("color_primary_dark")
editor.remove("color_primary_dark") settings.remove("color_accent")
editor.remove("color_accent") settings.remove("color_accent_dark")
editor.remove("color_accent_dark") settings.remove("dark_theme")
editor.remove("dark_theme")
editor.apply()
requireActivity().recreate() requireActivity().recreate()
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)

View File

@ -2,8 +2,8 @@ package bou.amine.apps.readerforselfossv2.android.themes
import android.app.Activity import android.app.Activity
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import com.russhwolf.settings.Settings
class AppColors(a: Activity) { class AppColors(a: Activity) {
@ -16,30 +16,30 @@ class AppColors(a: Activity) {
val isDarkTheme: Boolean val isDarkTheme: Boolean
init { init {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(a) val settings = Settings()
colorPrimary = colorPrimary =
sharedPref.getInt( settings.getInt(
"color_primary", "color_primary",
a.resources.getColor(R.color.colorPrimary) a.resources.getColor(R.color.colorPrimary)
) )
colorPrimaryDark = colorPrimaryDark =
sharedPref.getInt( settings.getInt(
"color_primary_dark", "color_primary_dark",
a.resources.getColor(R.color.colorPrimaryDark) a.resources.getColor(R.color.colorPrimaryDark)
) )
colorAccent = colorAccent =
sharedPref.getInt( settings.getInt(
"color_accent", "color_accent",
a.resources.getColor(R.color.colorAccent) a.resources.getColor(R.color.colorAccent)
) )
colorAccentDark = colorAccentDark =
sharedPref.getInt( settings.getInt(
"color_accent_dark", "color_accent_dark",
a.resources.getColor(R.color.colorAccentDark) a.resources.getColor(R.color.colorAccentDark)
) )
isDarkTheme = isDarkTheme =
sharedPref.getBoolean( settings.getBoolean(
"dark_theme", "dark_theme",
false false
) )

View File

@ -3,28 +3,27 @@ package bou.amine.apps.readerforselfossv2.android.utils
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.android.LoginActivity 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 val baseUrl: String
get() = settings.getString("url", "")!! get() = settings.getString("url", "")
val userLogin: String val userLogin: String
get() = settings.getString("login", "")!! get() = settings.getString("login", "")
val userPassword: String val userPassword: String
get() = settings.getString("password", "")!! get() = settings.getString("password", "")
val httpUserLogin: String val httpUserLogin: String
get() = settings.getString("httpUserName", "")!! get() = settings.getString("httpUserName", "")
val httpUserPassword: String val httpUserPassword: String
get() = settings.getString("httpPassword", "")!! get() = settings.getString("httpPassword", "")
companion object { companion object {
const val settingsName = "paramsselfoss" const val settingsName = "paramsselfoss"
@ -47,11 +46,10 @@ class Config(c: Context) {
fun logoutAndRedirect( fun logoutAndRedirect(
c: Context, c: Context,
callingActivity: Activity, callingActivity: Activity,
editor: SharedPreferences.Editor,
baseUrlFail: Boolean = false baseUrlFail: Boolean = false
): Boolean { ): Boolean {
val settings = PreferenceManager.getDefaultSharedPreferences(c) val settings = Settings()
settings.edit().clear().commit() settings.clear()
val intent = Intent(c, LoginActivity::class.java) val intent = Intent(c, LoginActivity::class.java)
if (baseUrlFail) { if (baseUrlFail) {
intent.putExtra("baseUrlFail", baseUrlFail) intent.putExtra("baseUrlFail", baseUrlFail)

View File

@ -10,18 +10,17 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.text.Spannable import android.text.Spannable
import android.text.style.ClickableSpan import android.text.style.ClickableSpan
import androidx.browser.customtabs.CustomTabsIntent
import android.util.Patterns import android.util.Patterns
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.browser.customtabs.CustomTabsIntent
import bou.amine.apps.readerforselfossv2.android.R import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.android.ReaderActivity import bou.amine.apps.readerforselfossv2.android.ReaderActivity
import bou.amine.apps.readerforselfossv2.android.model.getLinkDecoded import bou.amine.apps.readerforselfossv2.android.model.getLinkDecoded
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.SearchService
import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp import bou.amine.apps.readerforselfossv2.utils.toStringUriWithHttp
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -79,12 +78,10 @@ fun Context.openItemUrlInternally(
linkDecoded: String, linkDecoded: String,
customTabsIntent: CustomTabsIntent, customTabsIntent: CustomTabsIntent,
articleViewer: Boolean, articleViewer: Boolean,
app: Activity, app: Activity
searchService: SearchService
) { ) {
if (articleViewer) { if (articleViewer) {
ReaderActivity.allItems = allItems ReaderActivity.allItems = allItems
searchService.position = currentItem
val intent = Intent(this, ReaderActivity::class.java) val intent = Intent(this, ReaderActivity::class.java)
intent.putExtra("currentItem", currentItem) intent.putExtra("currentItem", currentItem)
app.startActivity(intent) app.startActivity(intent)
@ -123,8 +120,7 @@ fun Context.openItemUrl(
customTabsIntent: CustomTabsIntent, customTabsIntent: CustomTabsIntent,
internalBrowser: Boolean, internalBrowser: Boolean,
articleViewer: Boolean, articleViewer: Boolean,
app: Activity, app: Activity
searchService: SearchService
) { ) {
if (!linkDecoded.isUrlValid()) { if (!linkDecoded.isUrlValid()) {
@ -143,8 +139,7 @@ fun Context.openItemUrl(
linkDecoded, linkDecoded,
customTabsIntent, customTabsIntent,
articleViewer, articleViewer,
app, app
searchService
) )
} else { } else {
this.openItemUrlInternalBrowser( this.openItemUrlInternalBrowser(

View File

@ -1,13 +1,13 @@
package bou.amine.apps.readerforselfossv2.android.utils.glide package bou.amine.apps.readerforselfossv2.android.utils.glide
import android.content.Context import android.content.Context
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.getUnsafeHttpClient import bou.amine.apps.readerforselfossv2.android.utils.getUnsafeHttpClient
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry import com.bumptech.glide.Registry
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.GlideModule import com.bumptech.glide.module.GlideModule
import com.russhwolf.settings.Settings
import java.io.InputStream import java.io.InputStream
class SelfSignedGlideModule : GlideModule { class SelfSignedGlideModule : GlideModule {
@ -18,8 +18,8 @@ class SelfSignedGlideModule : GlideModule {
override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) { override fun registerComponents(context: Context?, glide: Glide?, registry: Registry?) {
if (context != null) { if (context != null) {
val pref = context?.getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE) val settings = Settings()
if (pref.getBoolean("isSelfSignedCert", false)) { if (settings.getBoolean("isSelfSignedCert", false)) {
val client = getUnsafeHttpClient().build() val client = getUnsafeHttpClient().build()
registry?.append( registry?.append(

View File

@ -27,6 +27,15 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("io.ktor:ktor-client-auth:2.0.1") implementation("io.ktor:ktor-client-auth:2.0.1")
implementation("org.jsoup:jsoup:1.14.3") 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 { val commonTest by getting {

View File

@ -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<ApiDetailsService>() with singleton { ApiDetailsServiceImpl() }
bind<SelfossApi>() with singleton { SelfossApi(instance()) }
}

View File

@ -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<SelfossModel.Item>()
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<SelfossModel.Item> {
// 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<SelfossModel.Item> {
// 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<SelfossModel.Item>? =
api.getItems(itemType.type, 200, 0, tagFilter?.tag, sourceFilter?.id?.toLong(), searchFilter, null)
private fun appendItems(fetchedItems: List<SelfossModel.Item>) {
// 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<SelfossModel.Item>) {
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<SelfossModel.Tag>? {
// TODO: Check success, store in DB
return api.tags()
}
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? {
// TODO: Check success, store in DB
return api.spouts()
}
suspend fun getSources(): ArrayList<SelfossModel.Source>? {
// 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<Int>): 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
}

View File

@ -3,10 +3,6 @@ package bou.amine.apps.readerforselfossv2.rest
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* 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.plugins.cache.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.request.forms.* import io.ktor.client.request.forms.*
@ -18,43 +14,51 @@ import kotlinx.serialization.json.Json
class SelfossApi(private val apiDetailsService: ApiDetailsService) { class SelfossApi(private val apiDetailsService: ApiDetailsService) {
private val client = HttpClient() { var client = createHttpClient()
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()) {
install(Auth) { private fun createHttpClient(): HttpClient {
basic { return HttpClient {
credentials { install(ContentNegotiation) {
BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword()) install(HttpCache)
} json(Json {
sendWithoutRequest { prettyPrint = true
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
expectSuccess = false 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" "${apiDetailsService.getBaseUrl()}$path"
fun refreshLoginInformation() {
apiDetailsService.refresh()
client = createHttpClient()
}
suspend fun login(): SelfossModel.SuccessResponse? = suspend fun login(): SelfossModel.SuccessResponse? =
client.get(url("/login")) { client.get(url("/login")) {
@ -66,10 +70,10 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
type: String, type: String,
items: Int, items: Int,
offset: Int, offset: Int,
tag: String? = "", tag: String?,
source: Long? = null, source: Long?,
search: String? = "", search: String?,
updatedSince: String? = "" updatedSince: String?
): List<SelfossModel.Item>? = ): List<SelfossModel.Item>? =
client.get(url("/items")) { client.get(url("/items")) {
parameter("username", apiDetailsService.getUserName()) parameter("username", apiDetailsService.getUserName())
@ -164,7 +168,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
createSource(title, url, spout, tags, filter) createSource(title, url, spout, tags, filter)
} }
private suspend fun createSource( suspend fun createSource(
title: String, title: String,
url: String, url: String,
spout: String, spout: String,
@ -182,7 +186,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
} }
).body() ).body()
private suspend fun createSource2( suspend fun createSource2(
title: String, title: String,
url: String, url: String,
spout: String, spout: String,

View File

@ -6,4 +6,5 @@ interface ApiDetailsService {
fun getBaseUrl(): String fun getBaseUrl(): String
fun getUserName(): String fun getUserName(): String
fun getPassword(): String fun getPassword(): String
fun refresh()
} }

View File

@ -1,48 +1,52 @@
package bou.amine.apps.readerforselfossv2.android.service package bou.amine.apps.readerforselfossv2.service
import android.content.Context import com.russhwolf.settings.Settings
import android.content.SharedPreferences import io.github.aakira.napier.Napier
import android.util.Log
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
class AndroidApiDetailsService(c: Context) : ApiDetailsService { class ApiDetailsServiceImpl : ApiDetailsService {
val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(c) val settings: Settings = Settings()
private var _apiVersion: Int = -1 private var _apiVersion: Int = -1
private var _baseUrl: String = "" private var _baseUrl: String = ""
private var _userName: String = "" private var _userName: String = ""
private var _password: String = "" private var _password: String = ""
override fun logApiCalls(message: String) { override fun logApiCalls(message: String) {
Log.d("LogApiCalls", message) Napier.d(message, tag = "LogApiCalls")
} }
override fun getApiVersion(): Int { override fun getApiVersion(): Int {
if (_apiVersion == -1) { if (_apiVersion == -1) {
_apiVersion = settings.getInt("apiVersionMajor", -1)!! _apiVersion = settings.getInt("apiVersionMajor", -1)
return settings.getInt("apiVersionMajor", -1)!! return _apiVersion
} }
return _apiVersion return _apiVersion
} }
override fun getBaseUrl(): String { override fun getBaseUrl(): String {
if (_baseUrl.isEmpty()) { if (_baseUrl.isEmpty()) {
_baseUrl = settings.getString("url", "")!! _baseUrl = settings.getString("url", "")
} }
return _baseUrl return _baseUrl
} }
override fun getUserName(): String { override fun getUserName(): String {
if (_userName.isEmpty()) { if (_userName.isEmpty()) {
_userName = settings.getString("login", "")!! _userName = settings.getString("login", "")
} }
return _userName return _userName
} }
override fun getPassword(): String { override fun getPassword(): String {
if (_password.isEmpty()) { if (_password.isEmpty()) {
_password = settings.getString("password", "")!! _password = settings.getString("password", "")
} }
return _password return _password
} }
override fun refresh() {
_password = settings.getString("password", "")
_userName = settings.getString("login", "")
_baseUrl = settings.getString("url", "")
_apiVersion = settings.getInt("apiVersionMajor", -1)
}
} }

View File

@ -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<ItemEntity>(val api: SelfossApi, private val dbService: DeviceDataBaseService<ItemEntity>, private val searchService: SearchService) {
suspend fun getReadItems(itemsNumber: Int, offset: Int, isNetworkAvailable: Boolean): List<SelfossModel.Item>? = 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<SelfossModel.Item>? = 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<SelfossModel.Item>? = 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<String>, 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<SelfossModel.Item>?, clearDatabase: Boolean) {
if (response != null) {
if (clearDatabase) {
CoroutineScope(Dispatchers.Default).launch {
dbService.clearDBItems()
}
}
dbService.appendNewItems(response)
}
}
suspend fun allNewItems(): List<SelfossModel.Item>? =
readItems(200, 0)
suspend fun allReadItems(): List<SelfossModel.Item>? =
newItems(200, 0)
suspend fun allStarredItems(): List<SelfossModel.Item>? =
starredItems(200, 0)
private suspend fun readItems(
itemsNumber: Int,
offset: Int
): List<SelfossModel.Item>? =
api.getItems("read", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter)
private suspend fun newItems(
itemsNumber: Int,
offset: Int
): List<SelfossModel.Item>? =
api.getItems("unread", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter)
private suspend fun starredItems(
itemsNumber: Int,
offset: Int
): List<SelfossModel.Item>? =
api.getItems("starred", itemsNumber, offset, searchService.tagFilter, searchService.sourceIDFilter, searchService.searchFilter)
}

View File

@ -2,7 +2,6 @@ package bou.amine.apps.readerforselfossv2.utils
import android.text.format.DateUtils import android.text.format.DateUtils
import bou.amine.apps.readerforselfossv2.rest.SelfossModel import bou.amine.apps.readerforselfossv2.rest.SelfossModel
import bou.amine.apps.readerforselfossv2.service.ApiDetailsService
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime 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 = fun SelfossModel.Item.parseRelativeDate(dateUtils: bou.amine.apps.readerforselfossv2.utils.DateUtils): String =
dateUtils.parseRelativeDate(this.datetime) dateUtils.parseRelativeDate(this.datetime)
class DateUtils(private val apiDetailsService: ApiDetailsService) { class DateUtils(private val apiMajorVersion: Int) {
fun parseDate(dateString: String): Instant { fun parseDate(dateString: String): Instant {
val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss" val FORMATTERV1 = "yyyy-MM-dd HH:mm:ss"
return if (apiDetailsService.getApiVersion() >= 4) { return if (apiMajorVersion >= 4) {
OffsetDateTime.parse(dateString).toInstant() OffsetDateTime.parse(dateString).toInstant()
} else { } else {
LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC) LocalDateTime.parse(dateString, DateTimeFormatter.ofPattern(FORMATTERV1)).toInstant(ZoneOffset.UTC)

View File

@ -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 }
}
}