Show reason why manga has no chapters

This commit is contained in:
Koitharu
2025-07-30 14:06:45 +03:00
parent fe21af5443
commit 1b76f21507
8 changed files with 64 additions and 27 deletions

View File

@@ -104,14 +104,14 @@ class CaptchaHandler @Inject constructor(
val dao = databaseProvider.get().getSourcesDao() val dao = databaseProvider.get().getSourcesDao()
dao.setCfState(source.name, exception?.state ?: CloudFlareHelper.PROTECTION_NOT_DETECTED) dao.setCfState(source.name, exception?.state ?: CloudFlareHelper.PROTECTION_NOT_DETECTED)
val exceptions = dao.findAllCaptchaRequired().mapNotNull {
it.source.toMangaSourceOrNull()
}.filterNot {
SourceSettings(context, it).isCaptchaNotificationsDisabled
}.mapNotNull {
exceptionMap[it]
}
if (notify && context.checkNotificationPermission(CHANNEL_ID)) { if (notify && context.checkNotificationPermission(CHANNEL_ID)) {
val exceptions = dao.findAllCaptchaRequired().mapNotNull {
it.source.toMangaSourceOrNull()
}.filterNot {
SourceSettings(context, it).isCaptchaNotificationsDisabled
}.mapNotNull {
exceptionMap[it]
}
if (removedException != null) { if (removedException != null) {
NotificationManagerCompat.from(context).cancel(TAG, removedException.source.hashCode()) NotificationManagerCompat.from(context).cancel(TAG, removedException.source.hashCode())
} }

View File

@@ -39,7 +39,7 @@ class ChapterPagesMenuProvider(
setOnActionExpandListener(this@ChapterPagesMenuProvider) setOnActionExpandListener(this@ChapterPagesMenuProvider)
(actionView as? SearchView)?.setupChaptersSearchView() (actionView as? SearchView)?.setupChaptersSearchView()
} }
menu.findItem(R.id.action_search)?.isVisible = viewModel.isChaptersEmpty.value == false menu.findItem(R.id.action_search)?.isVisible = viewModel.emptyReason.value == null
menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true menu.findItem(R.id.action_reversed)?.isChecked = viewModel.isChaptersReversed.value == true
menu.findItem(R.id.action_grid_view)?.isChecked = viewModel.isChaptersInGridView.value == true menu.findItem(R.id.action_grid_view)?.isChecked = viewModel.isChaptersInGridView.value == true
menu.findItem(R.id.action_downloaded)?.let { menuItem -> menu.findItem(R.id.action_downloaded)?.let { menuItem ->

View File

@@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
@@ -43,6 +44,7 @@ import org.koitharu.kotatsu.list.domain.ListFilterOption
import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase import org.koitharu.kotatsu.local.domain.DeleteLocalMangaUseCase
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.ReaderViewModel
@@ -97,9 +99,19 @@ abstract class ChaptersPagesViewModel(
} }
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0) }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
val isChaptersEmpty: StateFlow<Boolean> = mangaDetails.map { val emptyReason: StateFlow<EmptyMangaReason?> = combine(
it != null && it.isLoaded && it.allChapters.isEmpty() mangaDetails,
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), false) isLoading,
onError.onStart { emit(null) },
) { details, loading, error ->
when {
details == null || loading -> null
details.chapters.isNotEmpty() -> null
details.toManga().state == MangaState.RESTRICTED -> EmptyMangaReason.RESTRICTED
error != null -> EmptyMangaReason.LOADING_ERROR
else -> EmptyMangaReason.NO_CHAPTERS
}
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(), null)
val bookmarks = mangaDetails.flatMapLatest { val bookmarks = mangaDetails.flatMapLatest {
if (it != null) { if (it != null) {

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.details.ui.pager
import androidx.annotation.StringRes
import org.koitharu.kotatsu.R
enum class EmptyMangaReason(
@StringRes val msgResId: Int,
) {
NO_CHAPTERS(R.string.no_chapters_in_manga),
LOADING_ERROR(R.string.chapters_load_failed),
RESTRICTED(R.string.manga_restricted_description),
}

View File

@@ -30,6 +30,7 @@ import org.koitharu.kotatsu.core.util.RecyclerViewScrollCallback
import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.findParentCallback import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.databinding.FragmentChaptersBinding import org.koitharu.kotatsu.databinding.FragmentChaptersBinding
import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter
import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration
@@ -96,8 +97,8 @@ class ChaptersFragment :
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
.observe(viewLifecycleOwner, this::onChaptersChanged) .observe(viewLifecycleOwner, this::onChaptersChanged)
viewModel.quickFilter.observe(viewLifecycleOwner, this::onFilterChanged) viewModel.quickFilter.observe(viewLifecycleOwner, this::onFilterChanged)
viewModel.isChaptersEmpty.observe(viewLifecycleOwner) { viewModel.emptyReason.observe(viewLifecycleOwner) {
binding.textViewHolder.isVisible = it binding.textViewHolder.setTextAndVisible(it?.msgResId ?: 0)
} }
} }

View File

@@ -11,7 +11,6 @@ import androidx.appcompat.view.ActionMode
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -37,9 +36,11 @@ import org.koitharu.kotatsu.core.util.ext.findAppCompatDelegate
import org.koitharu.kotatsu.core.util.ext.findParentCallback import org.koitharu.kotatsu.core.util.ext.findParentCallback
import org.koitharu.kotatsu.core.util.ext.observe import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.setTextAndVisible
import org.koitharu.kotatsu.core.util.ext.showOrHide import org.koitharu.kotatsu.core.util.ext.showOrHide
import org.koitharu.kotatsu.databinding.FragmentPagesBinding import org.koitharu.kotatsu.databinding.FragmentPagesBinding
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
import org.koitharu.kotatsu.list.ui.GridSpanResolver import org.koitharu.kotatsu.list.ui.GridSpanResolver
import org.koitharu.kotatsu.list.ui.adapter.ListItemType import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
@@ -125,11 +126,18 @@ class PagesFragment :
it.spanCount = checkNotNull(spanResolver).spanCount it.spanCount = checkNotNull(spanResolver).spanCount
} }
} }
parentViewModel.isChaptersEmpty.observe(viewLifecycleOwner, ::onNoChaptersChanged) parentViewModel.emptyReason.observe(viewLifecycleOwner, ::onNoChaptersChanged)
viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged) viewModel.thumbnails.observe(viewLifecycleOwner, ::onThumbnailsChanged)
viewModel.onPageSaved.observeEvent(this, PagesSavedObserver(binding.recyclerView)) viewModel.onPageSaved.observeEvent(this, PagesSavedObserver(binding.recyclerView))
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this)) viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(binding.recyclerView, this))
viewModel.isLoading.observe(viewLifecycleOwner) { binding.progressBar.showOrHide(it) } combine(
viewModel.isLoading,
viewModel.thumbnails,
) { loading, content ->
loading && content.isEmpty()
}.observe(viewLifecycleOwner) {
binding.progressBar.showOrHide(it)
}
viewModel.isLoadingUp.observe(viewLifecycleOwner) { binding.progressBarTop.showOrHide(it) } viewModel.isLoadingUp.observe(viewLifecycleOwner) { binding.progressBarTop.showOrHide(it) }
viewModel.isLoadingDown.observe(viewLifecycleOwner) { binding.progressBarBottom.showOrHide(it) } viewModel.isLoadingDown.observe(viewLifecycleOwner) { binding.progressBarBottom.showOrHide(it) }
} }
@@ -237,10 +245,10 @@ class PagesFragment :
spanResolver?.setGridSize(scale, requireViewBinding().recyclerView) spanResolver?.setGridSize(scale, requireViewBinding().recyclerView)
} }
private fun onNoChaptersChanged(isNoChapters: Boolean) { private fun onNoChaptersChanged(reason: EmptyMangaReason?) {
with(viewBinding ?: return) { with(viewBinding ?: return) {
textViewHolder.isVisible = isNoChapters textViewHolder.setTextAndVisible(reason?.msgResId ?: 0)
recyclerView.isInvisible = isNoChapters recyclerView.isInvisible = reason != null
} }
} }

View File

@@ -5,8 +5,9 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
@@ -47,16 +48,15 @@ class PagesViewModel @Inject constructor(
) )
init { init {
loadingJob = launchLoadingJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val firstState = state.firstNotNull() state.filterNotNull()
doInit(firstState) .collect {
launchJob(Dispatchers.Default) { val prevJob = loadingJob
state.collectLatest { loadingJob = launchLoadingJob(Dispatchers.Default) {
if (it != null) { prevJob?.cancelAndJoin()
doInit(it) doInit(it)
} }
} }
}
} }
} }

View File

@@ -875,4 +875,7 @@
<string name="invalid_token">Invalid token: %s</string> <string name="invalid_token">Invalid token: %s</string>
<string name="show_floating_control_button">Show floating control button</string> <string name="show_floating_control_button">Show floating control button</string>
<string name="unavailable">Unavailable</string> <string name="unavailable">Unavailable</string>
<string name="manga_restricted_description">This manga is not available to read in this source. Try searching for it in other sources or opening it in a browser for more information</string>
<string name="no_chapters_in_manga">This manga does not contain any chapters</string>
<string name="chapters_load_failed">Failed to load chapter list</string>
</resources> </resources>