Handle offline mode in shelf

pull/242/head
Koitharu 4 years ago
parent 9b54ed6bc7
commit 74717e2b93
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -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 package org.koitharu.kotatsu.explore.ui.model
import android.net.Uri
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
@ -73,6 +72,7 @@ sealed interface ExploreItem : ListModel {
} }
} }
@Deprecated("")
class EmptyHint( class EmptyHint(
@DrawableRes icon: Int, @DrawableRes icon: Int,
@StringRes textPrimary: Int, @StringRes textPrimary: Int,
@ -81,4 +81,4 @@ sealed interface ExploreItem : ListModel {
) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem ) : EmptyState(icon, textPrimary, textSecondary, actionStringRes), ExploreItem
object Loading : 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 package org.koitharu.kotatsu.shelf.ui
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -113,7 +116,14 @@ class ShelfFragment :
override fun onRetryClick(error: Throwable) = Unit 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) { override fun onWindowInsetsChanged(insets: Insets) {
binding.recyclerView.updatePadding( 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.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.model.FavouriteCategory 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.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository 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.MangaWithHistory
import org.koitharu.kotatsu.history.domain.PROGRESS_NONE import org.koitharu.kotatsu.history.domain.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ListExtraProvider 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.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState 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.toGridModel
import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.parsers.model.Manga 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.parsers.model.SortOrder
import org.koitharu.kotatsu.shelf.domain.ShelfRepository import org.koitharu.kotatsu.shelf.domain.ShelfRepository
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
@ -41,17 +44,19 @@ class ShelfViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository, private val favouritesRepository: FavouritesRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val networkStateObserver: NetworkStateObserver,
) : BaseViewModel(), ListExtraProvider { ) : BaseViewModel(), ListExtraProvider {
val onActionDone = SingleLiveEvent<ReversibleAction>() val onActionDone = SingleLiveEvent<ReversibleAction>()
val content: LiveData<List<ListModel>> = combine( val content: LiveData<List<ListModel>> = combine(
networkStateObserver,
historyRepository.observeAllWithHistory(), historyRepository.observeAllWithHistory(),
repository.observeLocalManga(SortOrder.UPDATED), repository.observeLocalManga(SortOrder.UPDATED),
repository.observeFavourites(), repository.observeFavourites(),
trackingRepository.observeUpdatedManga(), trackingRepository.observeUpdatedManga(),
) { history, local, favourites, updated -> ) { isConnected, history, local, favourites, updated ->
mapList(history, favourites, updated, local) mapList(history, favourites, updated, local, isConnected)
}.debounce(500) }.debounce(500)
.catch { e -> .catch { e ->
emit(listOf(e.toErrorState(canRetry = false))) emit(listOf(e.toErrorState(canRetry = false)))
@ -133,19 +138,36 @@ class ShelfViewModel @Inject constructor(
favourites: Map<FavouriteCategory, List<Manga>>, favourites: Map<FavouriteCategory, List<Manga>>,
updated: Map<Manga, Int>, updated: Map<Manga, Int>,
local: List<Manga>, local: List<Manga>,
isNetworkAvailable: Boolean,
): List<ListModel> { ): List<ListModel> {
val result = ArrayList<ListModel>(favourites.keys.size + 2) val result = ArrayList<ListModel>(favourites.keys.size + 3)
if (history.isNotEmpty()) { if (isNetworkAvailable) {
mapHistory(result, history) if (history.isNotEmpty()) {
} mapHistory(result, history)
if (local.isNotEmpty()) { }
mapLocal(result, local) if (local.isNotEmpty()) {
} mapLocal(result, local)
if (updated.isNotEmpty()) { }
mapUpdated(result, updated) if (updated.isNotEmpty()) {
} mapUpdated(result, updated)
if (favourites.isNotEmpty()) { }
mapFavourites(result, favourites) 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()) { if (result.isEmpty()) {
result += EmptyState( result += EmptyState(
@ -154,6 +176,11 @@ class ShelfViewModel @Inject constructor(
textSecondary = R.string.text_shelf_holder_secondary, textSecondary = R.string.text_shelf_holder_secondary,
actionStringRes = 0, actionStringRes = 0,
) )
} else {
val one = result.singleOrNull()
if (one is EmptyHint) {
result[0] = one.toState()
}
} }
return result return result
} }

@ -6,16 +6,17 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter 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.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.fastscroll.FastScroller 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.ItemSizeResolver
import org.koitharu.kotatsu.list.ui.adapter.emptyHintAD
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
import kotlin.jvm.internal.Intrinsics
class ShelfAdapter( class ShelfAdapter(
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,
@ -40,6 +41,7 @@ class ShelfAdapter(
) )
.addDelegate(loadingStateAD()) .addDelegate(loadingStateAD())
.addDelegate(loadingFooterAD()) .addDelegate(loadingFooterAD())
.addDelegate(emptyHintAD(listener))
.addDelegate(emptyStateListAD(coil, listener)) .addDelegate(emptyStateListAD(coil, listener))
.addDelegate(errorStateListAD(listener)) .addDelegate(errorStateListAD(listener))
} }
@ -56,6 +58,7 @@ class ShelfAdapter(
oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> { oldItem is ShelfSectionModel && newItem is ShelfSectionModel -> {
oldItem.key == newItem.key oldItem.key == newItem.key
} }
else -> oldItem.javaClass == newItem.javaClass else -> oldItem.javaClass == newItem.javaClass
} }
} }

@ -44,6 +44,14 @@ val Context.activityManager: ActivityManager?
val Context.connectivityManager: ConnectivityManager val Context.connectivityManager: ConnectivityManager
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as 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) fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this)
suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable {

@ -393,4 +393,6 @@
<string name="webtoon_zoom">Webtoon zoom</string> <string name="webtoon_zoom">Webtoon zoom</string>
<string name="webtoon_zoom_summary">Allow zoom in/zoom out gesture in webtoon mode (beta)</string> <string name="webtoon_zoom_summary">Allow zoom in/zoom out gesture in webtoon mode (beta)</string>
<string name="different_languages">Different languages</string> <string name="different_languages">Different languages</string>
<string name="network_unavailable">Network is not available</string>
<string name="network_unavailable_hint">Turn on Wi-Fi or mobile network to read manga online</string>
</resources> </resources>

Loading…
Cancel
Save