Handle offline mode in shelf

This commit is contained in:
Koitharu
2022-10-14 17:04:04 +03:00
parent 9b54ed6bc7
commit 74717e2b93
9 changed files with 170 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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