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("androidx.viewpager2:viewpager2:1.1.0-beta01")
//Dependency Injection
implementation("org.kodein.di:kodein-di:7.12.0")
implementation("org.kodein.di:kodein-di-framework-android-x:7.12.0")
//Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
//PhotoView
implementation("com.github.chrisbanes:PhotoView:2.3.0")

View File

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

View File

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

View File

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

View File

@ -6,31 +6,35 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import androidx.preference.PreferenceManager
import android.widget.ImageView
import androidx.multidex.MultiDexApplication
import androidx.preference.PreferenceManager
import bou.amine.apps.readerforselfossv2.DI.networkModule
import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.glide.loadMaybeBasicAuth
import bou.amine.apps.readerforselfossv2.repository.Repository
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.ftinc.scoop.Scoop
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader
import java.util.UUID.randomUUID
import com.russhwolf.settings.Settings
import org.kodein.di.*
class MyApp : MultiDexApplication(), DIAware {
override val di by DI.lazy {
import(networkModule)
bind<Repository>() with singleton { Repository(instance(), instance()) }
}
class MyApp : MultiDexApplication() {
private lateinit var config: Config
private lateinit var settings : Settings
override fun onCreate() {
super.onCreate()
config = Config(baseContext)
val prefs = getSharedPreferences(Config.settingsName, Context.MODE_PRIVATE)
if (prefs.getString("unique_id", "")!!.isEmpty()) {
val editor = prefs.edit()
editor.putString("unique_id", randomUUID().toString())
editor.apply()
}
config = Config()
settings = Settings()
initDrawerImageLoader()

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,15 @@ kotlin {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
implementation("io.ktor:ktor-client-auth:2.0.1")
implementation("org.jsoup:jsoup:1.14.3")
//Dependency Injection
implementation("org.kodein.di:kodein-di:7.12.0")
//Settings
implementation("com.russhwolf:multiplatform-settings-no-arg:0.9")
//Logging
implementation("io.github.aakira:napier:2.6.1")
}
}
val commonTest by getting {

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 io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.*
import io.ktor.client.engine.ProxyBuilder.http
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.cache.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
@ -18,43 +14,51 @@ import kotlinx.serialization.json.Json
class SelfossApi(private val apiDetailsService: ApiDetailsService) {
private val client = HttpClient() {
install(ContentNegotiation) {
install(HttpCache)
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object: Logger {
override fun log(message: String) {
apiDetailsService.logApiCalls(message)
}
}
level = LogLevel.ALL
}
/* TODO: Auth as basic
if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) {
var client = createHttpClient()
install(Auth) {
basic {
credentials {
BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword())
}
sendWithoutRequest {
true
private fun createHttpClient(): HttpClient {
return HttpClient {
install(ContentNegotiation) {
install(HttpCache)
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
apiDetailsService.logApiCalls(message)
}
}
level = LogLevel.ALL
}
}*/
expectSuccess = false
/* TODO: Auth as basic
if (apiDetailsService.getUserName().isNotEmpty() && apiDetailsService.getPassword().isNotEmpty()) {
install(Auth) {
basic {
credentials {
BasicAuthCredentials(username = apiDetailsService.getUserName(), password = apiDetailsService.getPassword())
}
sendWithoutRequest {
true
}
}
}
}*/
expectSuccess = false
}
}
private fun url(path: String) =
fun url(path: String) =
"${apiDetailsService.getBaseUrl()}$path"
fun refreshLoginInformation() {
apiDetailsService.refresh()
client = createHttpClient()
}
suspend fun login(): SelfossModel.SuccessResponse? =
client.get(url("/login")) {
@ -66,10 +70,10 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
type: String,
items: Int,
offset: Int,
tag: String? = "",
source: Long? = null,
search: String? = "",
updatedSince: String? = ""
tag: String?,
source: Long?,
search: String?,
updatedSince: String?
): List<SelfossModel.Item>? =
client.get(url("/items")) {
parameter("username", apiDetailsService.getUserName())
@ -164,7 +168,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
createSource(title, url, spout, tags, filter)
}
private suspend fun createSource(
suspend fun createSource(
title: String,
url: String,
spout: String,
@ -182,7 +186,7 @@ class SelfossApi(private val apiDetailsService: ApiDetailsService) {
}
).body()
private suspend fun createSource2(
suspend fun createSource2(
title: String,
url: String,
spout: String,

View File

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

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

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