Compare commits

...

3 Commits

4 changed files with 217 additions and 206 deletions

View File

@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.view.doOnNextLayout
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.*
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
@ -34,10 +35,13 @@ import bou.amine.apps.readerforselfossv2.android.utils.Config
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.maybeShow
import bou.amine.apps.readerforselfossv2.android.utils.bottombar.removeBadge
import bou.amine.apps.readerforselfossv2.android.utils.customtabs.CustomTabActivityHelper
import bou.amine.apps.readerforselfossv2.dao.ACTION
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.android.viewmodel.AppViewModel
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.utils.*
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.ItemType
import bou.amine.apps.readerforselfossv2.utils.getHtmlDecoded
import bou.amine.apps.readerforselfossv2.utils.getIcon
import bou.amine.apps.readerforselfossv2.utils.longHash
import com.ashokvarma.bottomnavigation.BottomNavigationBar
import com.ashokvarma.bottomnavigation.BottomNavigationItem
import com.ashokvarma.bottomnavigation.TextBadgeItem
@ -66,7 +70,6 @@ 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, DIAware {
@ -118,6 +121,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
override val di by closestDI()
private val repository : Repository by instance()
private val viewModel: AppViewModel by instance()
data class DrawerData(val tags: List<SelfossModel.Tag>?, val sources: List<SelfossModel.Source>?)
@ -142,6 +146,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
setContentView(view)
lifecycleScope.launch {
viewModel.refreshingIndicatorProvider.collect { showRefresh ->
binding.swipeRefreshLayout.isRefreshing = showRefresh
}
}
handleThemeBinding()
@ -153,18 +162,25 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
mDrawerToggle.syncState()
customTabActivityHelper = CustomTabActivityHelper()
handleSettings()
lifecycleScope.launch {
viewModel.items.collect { fetchedItems ->
items = fetchedItems
handleListResult()
}
}
handleBottomBar()
handleDrawer()
handleSwipeRefreshLayout()
handleSettings()
getElementsAccordingToTab()
handleBadgesContent()
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
repository.tryToCacheItemsAndGetNewOnes()
}
@ -180,10 +196,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
repository.offlineOverride = false
lastFetchDone = false
handleDrawerItems()
CoroutineScope(Dispatchers.Main).launch {
getElementsAccordingToTab()
binding.swipeRefreshLayout.isRefreshing = false
}
viewModel.getItems(false, elementsShown)
}
val simpleItemTouchCallback =
@ -219,8 +232,6 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
adapter.handleItemAtIndex(position)
reloadBadgeContent()
val tagHashes = i.tags.map { it.longHash() }
tagsBadge = tagsBadge.map {
if (tagHashes.contains(it.key)) {
@ -252,16 +263,23 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
tabNewBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.setBackgroundColor(appColors.colorPrimary)
if (!displayUnreadCount) {
tabNewBadge.hide(false)
}
tabArchiveBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.setBackgroundColor(appColors.colorPrimary)
tabStarredBadge = TextBadgeItem()
.setText("")
.setHideOnSelect(false).hide(false)
.setHideOnSelect(false)
.setBackgroundColor(appColors.colorPrimary)
if (!displayAllCount) {
tabArchiveBadge.hide(false)
tabStarredBadge.hide(false)
}
val tabNew =
BottomNavigationItem(
@ -318,7 +336,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
handleRecurringTask()
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
repository.handleDBActions()
}
@ -453,7 +471,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
// TODO: refactor this.
private fun handleDrawerItems() {
tagsBadge = emptyMap()
fun handleDrawerData(maybeDrawerData: DrawerData?, loadedFromCache: Boolean = false) {
fun handleDrawerData(maybeDrawerData: DrawerData?) {
fun createDrawerItem(
it: SelfossModel.Tag
) {
@ -495,12 +513,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fun handleTags(maybeTags: List<SelfossModel.Tag>?) {
if (maybeTags == null) {
if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem()
.apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false }
)
}
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem()
.apply { nameRes = R.string.drawer_error_loading_tags; isSelectable = false }
)
} else {
val filteredTags = maybeTags
.filterNot { hiddenTags.contains(it.tag) }
@ -515,14 +531,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fun handleHiddenTags(maybeTags: List<SelfossModel.Tag>?) {
if (maybeTags == null) {
if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_tags
isSelectable = false
}
)
}
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_tags
isSelectable = false
}
)
} else {
val filteredHiddenTags: List<SelfossModel.Tag> =
maybeTags.filter { hiddenTags.contains(it.tag) }
@ -536,14 +550,12 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
fun handleSources(maybeSources: List<SelfossModel.Source>?) {
if (maybeSources == null) {
if (loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_sources
isSelectable = false
}
)
}
binding.mainDrawer.itemAdapter.add(
SecondaryDrawerItem().apply {
nameRes = R.string.drawer_error_loading_sources
isSelectable = false
}
)
} else {
for (source in maybeSources) {
val item = PrimaryDrawerItem().apply {
@ -632,61 +644,19 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
)
if (!loadedFromCache) {
if (maybeDrawerData.tags != null) {
thread {
repository.resetDBTagsWithData(maybeDrawerData.tags)
}
}
if (maybeDrawerData.sources != null) {
thread {
repository.resetDBSourcesWithData(maybeDrawerData.sources)
}
}
}
} else {
if (!loadedFromCache) {
binding.mainDrawer.itemAdapter.add(
PrimaryDrawerItem().apply {
nameRes = R.string.no_tags_loaded
identifier = DRAWER_ID_TAGS
isSelectable = false
},
PrimaryDrawerItem().apply {
nameRes = R.string.no_sources_loaded
identifier = DRAWER_ID_SOURCES
isSelectable = false
}
)
}
}
}
fun drawerApiCalls(maybeDrawerData: DrawerData?) {
var tags: List<SelfossModel.Tag>? = null
var sources: List<SelfossModel.Source>?
fun sourcesApiCall() {
CoroutineScope(Dispatchers.Main).launch {
val response = repository.getSources()
if (response != null) {
sources = response
val apiDrawerData = DrawerData(tags, sources)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
} else {
val apiDrawerData = DrawerData(tags, null)
if ((maybeDrawerData != null && maybeDrawerData != apiDrawerData) || maybeDrawerData == null) {
handleDrawerData(apiDrawerData)
}
binding.mainDrawer.itemAdapter.add(
PrimaryDrawerItem().apply {
nameRes = R.string.no_tags_loaded
identifier = DRAWER_ID_TAGS
isSelectable = false
},
PrimaryDrawerItem().apply {
nameRes = R.string.no_sources_loaded
identifier = DRAWER_ID_SOURCES
isSelectable = false
}
}
}
CoroutineScope(Dispatchers.IO).launch {
tags = repository.getTags()
sourcesApiCall()
)
}
}
@ -697,12 +667,11 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
)
thread {
val drawerData = DrawerData(repository.getDBTags().map { it.toView() },
repository.getDBSources().map { it.toView() })
CoroutineScope(Dispatchers.IO).launch {
val drawerData = DrawerData(repository.getTags(),
repository.getSources())
runOnUiThread {
handleDrawerData(drawerData, loadedFromCache = true)
drawerApiCalls(drawerData)
handleDrawerData(drawerData)
}
}
}
@ -839,21 +808,7 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
}
firstVisible = if (appendResults) firstVisible else 0
getItems(appendResults, elementsShown)
}
private fun getItems(appendResults: Boolean, itemType: ItemType) {
CoroutineScope(Dispatchers.Main).launch {
binding.swipeRefreshLayout.isRefreshing = true
repository.displayedItems = itemType
items = if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
binding.swipeRefreshLayout.isRefreshing = false
handleListResult()
}
viewModel.getItems(appendResults, elementsShown)
}
private fun handleListResult(appendResults: Boolean = false) {
@ -915,26 +870,50 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun reloadBadges() {
if (displayUnreadCount || displayAllCount) {
CoroutineScope(Dispatchers.Main).launch {
CoroutineScope(Dispatchers.IO).launch {
repository.reloadBadges()
reloadBadgeContent()
}
}
}
private fun reloadBadgeContent() {
private fun handleBadgesContent() {
if (displayUnreadCount) {
tabNewBadge
.setText(repository.badgeUnread.toString())
.maybeShow()
lifecycleScope.launch {
repository.badgeUnread.collect { unreadCount ->
if (unreadCount > 0) {
tabNewBadge
.setText(unreadCount.toString())
.maybeShow()
} else {
tabNewBadge.removeBadge()
}
}
}
}
if (displayAllCount) {
tabArchiveBadge
.setText(repository.badgeAll.toString())
.maybeShow()
tabStarredBadge
.setText(repository.badgeStarred.toString())
.maybeShow()
lifecycleScope.launch {
repository.badgeAll.collect { itemsCount ->
if (itemsCount > 0) {
tabArchiveBadge
.setText(itemsCount.toString())
.maybeShow()
} else {
tabArchiveBadge.removeBadge()
}
}
}
lifecycleScope.launch {
repository.badgeStarred.collect { starredCount ->
if (starredCount > 0) {
tabStarredBadge
.setText(starredCount.toString())
.maybeShow()
} else {
tabStarredBadge.removeBadge()
}
}
}
}
}
@ -992,56 +971,14 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
when (item.itemId) {
R.id.refresh -> {
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 updatedRemote = repository.updateRemote()
if (updatedRemote) {
// TODO: Send toast messages from the repository
Toast.makeText(
this@HomeActivity,
R.string.refresh_success_response, Toast.LENGTH_LONG
)
.show()
} else {
Toast.makeText(
this@HomeActivity,
R.string.refresh_failer_message,
Toast.LENGTH_SHORT
).show()
}
}
viewModel.updateRemote()
}
return true
}
R.id.readAll -> {
if (elementsShown == ItemType.UNREAD) {
needsConfirmation(R.string.readAll, R.string.markall_dialog_message) {
binding.swipeRefreshLayout.isRefreshing = true
CoroutineScope(Dispatchers.Main).launch {
val success = repository.markAllAsRead(items)
if (success) {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_read,
Toast.LENGTH_SHORT
).show()
tabNewBadge.removeBadge()
handleDrawerItems()
getElementsAccordingToTab()
} else {
Toast.makeText(
this@HomeActivity,
R.string.all_posts_not_read,
Toast.LENGTH_SHORT
).show()
}
handleListResult()
binding.swipeRefreshLayout.isRefreshing = false
}
viewModel.markAllAsRead()
}
}
return true
@ -1055,10 +992,10 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
private fun maxItemNumber(): Int =
when (elementsShown) {
ItemType.UNREAD -> repository.badgeUnread
ItemType.ALL -> repository.badgeAll
ItemType.STARRED -> repository.badgeStarred
else -> repository.badgeUnread // if !elementsShown then unread are fetched.
ItemType.UNREAD -> repository.badgeUnread.value
ItemType.ALL -> repository.badgeAll.value
ItemType.STARRED -> repository.badgeStarred.value
else -> repository.badgeUnread.value // if !elementsShown then unread are fetched.
}
private fun updateItems(adapterItems: ArrayList<SelfossModel.Item>) {
@ -1082,9 +1019,4 @@ class HomeActivity : AppCompatActivity(), SearchView.OnQueryTextListener, DIAwar
WorkManager.getInstance(baseContext).enqueueUniquePeriodicWork("selfoss-loading", ExistingPeriodicWorkPolicy.KEEP, backgroundWork)
}
}
private fun handleOfflineActions() {
}
}

View File

@ -84,6 +84,15 @@ class MyApp : MultiDexApplication(), DIAware {
).show()
}
}
CoroutineScope(Dispatchers.Main).launch {
viewModel.toastMessageProvider.collect { toastMessage ->
Toast.makeText(
applicationContext,
toastMessage,
Toast.LENGTH_SHORT
).show()
}
}
}

View File

@ -3,16 +3,29 @@ package bou.amine.apps.readerforselfossv2.android.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import bou.amine.apps.readerforselfossv2.android.R
import bou.amine.apps.readerforselfossv2.model.SelfossModel
import bou.amine.apps.readerforselfossv2.repository.Repository
import bou.amine.apps.readerforselfossv2.utils.ItemType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AppViewModel(private val repository: Repository) : ViewModel() {
private val _networkAvailableProvider = MutableSharedFlow<Boolean>()
val networkAvailableProvider = _networkAvailableProvider.asSharedFlow()
private val _refreshingIndicatorProvider = MutableSharedFlow<Boolean>()
val refreshingIndicatorProvider = _refreshingIndicatorProvider.asSharedFlow()
private val _toastMessageProvider = MutableSharedFlow<Int>()
val toastMessageProvider = _toastMessageProvider.asSharedFlow()
private var wasConnected = true
private val _items = MutableStateFlow(ArrayList<SelfossModel.Item>())
val items = _items.asStateFlow()
init {
viewModelScope.launch {
repository.isConnectionAvailable.collect { isConnected ->
@ -28,4 +41,44 @@ class AppViewModel(private val repository: Repository) : ViewModel() {
}
}
}
fun updateRemote() {
CoroutineScope(Dispatchers.IO).launch {
_toastMessageProvider.emit(R.string.refresh_in_progress)
val updatedRemote = repository.updateRemote()
if (updatedRemote) {
_toastMessageProvider.emit(R.string.refresh_success_response)
} else {
_toastMessageProvider.emit(R.string.refresh_failer_message)
}
}
}
fun getItems(appendResults: Boolean, itemType: ItemType) {
// TODO: Find a way to use Dispatchers.IO without creating conflicts
CoroutineScope(Dispatchers.Main).launch {
_refreshingIndicatorProvider.emit(true)
repository.displayedItems = itemType
val items = if (appendResults) {
repository.getOlderItems()
} else {
repository.getNewerItems()
}
_items.emit(items)
_refreshingIndicatorProvider.emit(false)
}
}
fun markAllAsRead() {
CoroutineScope(Dispatchers.IO).launch {
_refreshingIndicatorProvider.emit(true)
val success = repository.markAllAsRead(items.value)
if (success) {
_toastMessageProvider.emit(R.string.all_posts_read)
} else {
_toastMessageProvider.emit(R.string.all_posts_not_read)
}
_refreshingIndicatorProvider.emit(false)
}
}
}

View File

@ -11,6 +11,8 @@ import com.russhwolf.settings.Settings
import io.github.aakira.napier.Napier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class Repository(private val api: SelfossApi, private val apiDetails: ApiDetailsService, private val connectivityStatus: ConnectivityStatus, private val db: ReaderForSelfossDB) {
@ -33,18 +35,23 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
var offlineOverride = 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 } }
private val _badgeUnread = MutableStateFlow(0)
val badgeUnread = _badgeUnread.asStateFlow()
private val _badgeAll = MutableStateFlow(0)
val badgeAll = _badgeAll.asStateFlow()
private val _badgeStarred = MutableStateFlow(0)
val badgeStarred = _badgeStarred.asStateFlow()
init {
// TODO: Dispatchers.IO not available in KMM, an alternative solution should be found
CoroutineScope(Dispatchers.Main).launch {
updateApiVersion()
reloadBadges()
isConnectionAvailable.collect { connectionAvailable ->
if (connectionAvailable) {
updateApiVersion()
reloadBadges()
}
}
}
}
@ -125,27 +132,31 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
if (isNetworkAvailable()) {
val response = api.stats()
if (response != null) {
badgeUnread = response.unread
badgeAll = response.total
badgeStarred = response.starred
_badgeUnread.value = response.unread
_badgeAll.value = response.total
_badgeStarred.value = response.starred
success = true
}
} else {
} else if (itemsCaching) {
// TODO: do this differently, because it's not efficient
val dbItems = getDBItems()
badgeUnread = dbItems.filter { item -> item.unread }.size
badgeStarred = dbItems.filter { item -> item.starred }.size
badgeAll = items.size
_badgeUnread.value = dbItems.filter { item -> item.unread }.size
_badgeStarred.value = dbItems.filter { item -> item.starred }.size
_badgeAll.value = dbItems.size
}
return success
}
suspend fun getTags(): List<SelfossModel.Tag>? {
return if (isNetworkAvailable()) {
val tags = if (isNetworkAvailable()) {
api.tags()
} else {
getDBTags().map { it.toView() }
}
if (tags != null) {
resetDBTagsWithData(tags)
}
return tags
}
suspend fun getSpouts(): Map<String, SelfossModel.Spout>? {
@ -157,12 +168,15 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
}
suspend fun getSources(): ArrayList<SelfossModel.Source>? {
return if (isNetworkAvailable()) {
val sources = if (isNetworkAvailable()) {
api.sources()
} else {
ArrayList(getDBSources().map { it.toView() })
}
if (sources != null) {
resetDBSourcesWithData(sources)
}
return sources
}
suspend fun markAsRead(item: SelfossModel.Item): Boolean {
@ -253,7 +267,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
private fun markAsReadLocally(item: SelfossModel.Item) {
if (item.unread) {
item.unread = false
badgeUnread -= 1
_badgeUnread.value -= 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -264,7 +278,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
private fun unmarkAsReadLocally(item: SelfossModel.Item) {
if (!item.unread) {
item.unread = true
badgeUnread += 1
_badgeUnread.value += 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -275,7 +289,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
private fun starrLocally(item: SelfossModel.Item) {
if (!item.starred) {
item.starred = true
badgeStarred += 1
_badgeStarred.value += 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -286,7 +300,7 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
private fun unstarrLocally(item: SelfossModel.Item) {
if (item.starred) {
item.starred = false
badgeStarred -= 1
_badgeStarred.value -= 1
}
CoroutineScope(Dispatchers.Main).launch {
@ -428,13 +442,16 @@ class Repository(private val api: SelfossApi, private val apiDetails: ApiDetails
suspend fun tryToCacheItemsAndGetNewOnes(): List<SelfossModel.Item>? {
try {
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
val allItems = getMaxItemsForBackground(ItemType.ALL)
val starredItems = getMaxItemsForBackground(ItemType.STARRED)
insertDBItems(newItems.orEmpty() + allItems.orEmpty() + starredItems.orEmpty())
return newItems
} catch (e: Throwable) {}
if (itemsCaching) {
try {
val newItems = getMaxItemsForBackground(ItemType.UNREAD)
val allItems = getMaxItemsForBackground(ItemType.ALL)
val starredItems = getMaxItemsForBackground(ItemType.STARRED)
insertDBItems(newItems.orEmpty() + allItems.orEmpty() + starredItems.orEmpty())
return newItems
} catch (e: Throwable) {
}
}
return emptyList()
}