Handle offline mode in shelf
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package org.koitharu.kotatsu.core.os
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkRequest
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.ProducerScope
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import org.koitharu.kotatsu.utils.ext.connectivityManager
|
||||
import org.koitharu.kotatsu.utils.ext.isNetworkAvailable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NetworkStateObserver @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
) : StateFlow<Boolean> {
|
||||
|
||||
private val connectivityManager = context.connectivityManager
|
||||
|
||||
override val replayCache: List<Boolean>
|
||||
get() = listOf(value)
|
||||
|
||||
override var value: Boolean = connectivityManager.isNetworkAvailable
|
||||
|
||||
override suspend fun collect(collector: FlowCollector<Boolean>): Nothing {
|
||||
collector.emit(value)
|
||||
while (true) {
|
||||
observeImpl().collect(collector)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeImpl() = callbackFlow<Boolean> {
|
||||
val request = NetworkRequest.Builder().build()
|
||||
val callback = FlowNetworkCallback(this)
|
||||
connectivityManager.registerNetworkCallback(request, callback)
|
||||
awaitClose {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
inner class FlowNetworkCallback(
|
||||
private val producerScope: ProducerScope<Boolean>,
|
||||
) : NetworkCallback() {
|
||||
override fun onAvailable(network: Network) = update()
|
||||
|
||||
override fun onLost(network: Network) = update()
|
||||
|
||||
override fun onUnavailable() = update()
|
||||
|
||||
private fun update() {
|
||||
val newValue = connectivityManager.isNetworkAvailable
|
||||
if (value != newValue) {
|
||||
value = newValue
|
||||
producerScope.trySendBlocking(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.koitharu.kotatsu.explore.ui.model
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
@@ -73,6 +72,7 @@ sealed interface ExploreItem : ListModel {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
class EmptyHint(
|
||||
@DrawableRes icon: Int,
|
||||
@StringRes textPrimary: Int,
|
||||
@@ -81,4 +81,4 @@ sealed interface ExploreItem : ListModel {
|
||||
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
|
||||
|
||||
object Loading : ExploreItem
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.koitharu.kotatsu.list.ui.adapter
|
||||
|
||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||
import org.koitharu.kotatsu.databinding.ItemEmptyCardBinding
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.utils.ext.setTextAndVisible
|
||||
|
||||
fun emptyHintAD(
|
||||
listener: ListStateHolderListener,
|
||||
) = adapterDelegateViewBinding<EmptyHint, ListModel, ItemEmptyCardBinding>(
|
||||
{ inflater, parent -> ItemEmptyCardBinding.inflate(inflater, parent, false) },
|
||||
) {
|
||||
|
||||
binding.buttonRetry.setOnClickListener { listener.onEmptyActionClick() }
|
||||
|
||||
bind {
|
||||
binding.icon.setImageResource(item.icon)
|
||||
binding.textPrimary.setText(item.textPrimary)
|
||||
binding.textSecondary.setTextAndVisible(item.textSecondary)
|
||||
binding.buttonRetry.setTextAndVisible(item.actionStringRes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.list.ui.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
class EmptyHint(
|
||||
@DrawableRes icon: Int,
|
||||
@StringRes textPrimary: Int,
|
||||
@StringRes textSecondary: Int,
|
||||
@StringRes actionStringRes: Int,
|
||||
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes) {
|
||||
|
||||
fun toState() = EmptyState(icon, textPrimary, textSecondary, actionStringRes)
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.koitharu.kotatsu.shelf.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -113,7 +116,14 @@ class ShelfFragment :
|
||||
|
||||
override fun onRetryClick(error: Throwable) = Unit
|
||||
|
||||
override fun onEmptyActionClick() = Unit
|
||||
override fun onEmptyActionClick() {
|
||||
val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
Settings.Panel.ACTION_INTERNET_CONNECTIVITY
|
||||
} else {
|
||||
Settings.ACTION_WIRELESS_SETTINGS
|
||||
}
|
||||
startActivity(Intent(action))
|
||||
}
|
||||
|
||||
override fun onWindowInsetsChanged(insets: Insets) {
|
||||
binding.recyclerView.updatePadding(
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.os.NetworkStateObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
@@ -19,6 +20,7 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.history.domain.MangaWithHistory
|
||||
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
|
||||
import org.koitharu.kotatsu.list.domain.ListExtraProvider
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
@@ -26,6 +28,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.list.ui.model.toGridModel
|
||||
import org.koitharu.kotatsu.list.ui.model.toUi
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.shelf.domain.ShelfRepository
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
@@ -41,17 +44,19 @@ class ShelfViewModel @Inject constructor(
|
||||
private val favouritesRepository: FavouritesRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
private val networkStateObserver: NetworkStateObserver,
|
||||
) : BaseViewModel(), ListExtraProvider {
|
||||
|
||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||
|
||||
val content: LiveData<List<ListModel>> = combine(
|
||||
networkStateObserver,
|
||||
historyRepository.observeAllWithHistory(),
|
||||
repository.observeLocalManga(SortOrder.UPDATED),
|
||||
repository.observeFavourites(),
|
||||
trackingRepository.observeUpdatedManga(),
|
||||
) { history, local, favourites, updated ->
|
||||
mapList(history, favourites, updated, local)
|
||||
) { isConnected, history, local, favourites, updated ->
|
||||
mapList(history, favourites, updated, local, isConnected)
|
||||
}.debounce(500)
|
||||
.catch { e ->
|
||||
emit(listOf(e.toErrorState(canRetry = false)))
|
||||
@@ -133,19 +138,36 @@ class ShelfViewModel @Inject constructor(
|
||||
favourites: Map<FavouriteCategory, List<Manga>>,
|
||||
updated: Map<Manga, Int>,
|
||||
local: List<Manga>,
|
||||
isNetworkAvailable: Boolean,
|
||||
): List<ListModel> {
|
||||
val result = ArrayList<ListModel>(favourites.keys.size + 2)
|
||||
if (history.isNotEmpty()) {
|
||||
mapHistory(result, history)
|
||||
}
|
||||
if (local.isNotEmpty()) {
|
||||
mapLocal(result, local)
|
||||
}
|
||||
if (updated.isNotEmpty()) {
|
||||
mapUpdated(result, updated)
|
||||
}
|
||||
if (favourites.isNotEmpty()) {
|
||||
mapFavourites(result, favourites)
|
||||
val result = ArrayList<ListModel>(favourites.keys.size + 3)
|
||||
if (isNetworkAvailable) {
|
||||
if (history.isNotEmpty()) {
|
||||
mapHistory(result, history)
|
||||
}
|
||||
if (local.isNotEmpty()) {
|
||||
mapLocal(result, local)
|
||||
}
|
||||
if (updated.isNotEmpty()) {
|
||||
mapUpdated(result, updated)
|
||||
}
|
||||
if (favourites.isNotEmpty()) {
|
||||
mapFavourites(result, favourites)
|
||||
}
|
||||
} else {
|
||||
result += EmptyHint(
|
||||
icon = R.drawable.ic_empty_suggestions,
|
||||
textPrimary = R.string.network_unavailable,
|
||||
textSecondary = R.string.network_unavailable_hint,
|
||||
actionStringRes = R.string.manage,
|
||||
)
|
||||
val offlineHistory = history.filter { it.manga.source == MangaSource.LOCAL }
|
||||
if (offlineHistory.isNotEmpty()) {
|
||||
mapHistory(result, offlineHistory)
|
||||
}
|
||||
if (local.isNotEmpty()) {
|
||||
mapLocal(result, local)
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result += EmptyState(
|
||||
@@ -154,6 +176,11 @@ class ShelfViewModel @Inject constructor(
|
||||
textSecondary = R.string.text_shelf_holder_secondary,
|
||||
actionStringRes = 0,
|
||||
)
|
||||
} else {
|
||||
val one = result.singleOrNull()
|
||||
if (one is EmptyHint) {
|
||||
result[0] = one.toState()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -6,16 +6,17 @@ import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
|
||||
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
import org.koitharu.kotatsu.list.ui.ItemSizeResolver
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
|
||||
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
|
||||
import kotlin.jvm.internal.Intrinsics
|
||||
|
||||
class ShelfAdapter(
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
@@ -40,6 +41,7 @@ class ShelfAdapter(
|
||||
)
|
||||
.addDelegate(loadingStateAD())
|
||||
.addDelegate(loadingFooterAD())
|
||||
.addDelegate(emptyHintAD(listener))
|
||||
.addDelegate(emptyStateListAD(coil, listener))
|
||||
.addDelegate(errorStateListAD(listener))
|
||||
}
|
||||
@@ -56,6 +58,7 @@ class ShelfAdapter(
|
||||
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> {
|
||||
oldItem.key == newItem.key
|
||||
}
|
||||
|
||||
else -> oldItem.javaClass == newItem.javaClass
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,14 @@ val Context.activityManager: ActivityManager?
|
||||
val Context.connectivityManager: ConnectivityManager
|
||||
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
val ConnectivityManager.isNetworkAvailable: Boolean
|
||||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
activeNetwork != null
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
activeNetworkInfo?.isConnectedOrConnecting == true
|
||||
}
|
||||
|
||||
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
|
||||
|
||||
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {
|
||||
|
||||
Reference in New Issue
Block a user