Notify about broken source on list screen

This commit is contained in:
2025-10-21 18:04:05 +03:00
parent 9e5664da3a
commit d0ed1fb85f
3 changed files with 116 additions and 95 deletions

View File

@@ -29,120 +29,133 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.search.domain.SearchKind import org.koitharu.kotatsu.search.domain.SearchKind
@AndroidEntryPoint @AndroidEntryPoint
class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner, View.OnClickListener {
override val viewModel by viewModels<RemoteListViewModel>() override val viewModel by viewModels<RemoteListViewModel>()
override val filterCoordinator: FilterCoordinator override val filterCoordinator: FilterCoordinator
get() = viewModel.filterCoordinator get() = viewModel.filterCoordinator
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState) super.onViewBindingCreated(binding, savedInstanceState)
addMenuProvider(RemoteListMenuProvider()) addMenuProvider(RemoteListMenuProvider())
addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel)) addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel))
viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity())) viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
viewModel.onOpenManga.observeEvent(viewLifecycleOwner) { router.openDetails(it) } viewModel.onOpenManga.observeEvent(viewLifecycleOwner) { router.openDetails(it) }
filterCoordinator.observe().distinctUntilChangedBy { it.listFilter.isEmpty() } viewModel.onSourceBroken.observeEvent(viewLifecycleOwner) { showSourceBrokenWarning() }
.drop(1) filterCoordinator.observe().distinctUntilChangedBy { it.listFilter.isEmpty() }
.observe(viewLifecycleOwner) { .drop(1)
activity?.invalidateMenu() .observe(viewLifecycleOwner) {
} activity?.invalidateMenu()
} }
}
override fun onScrolledToEnd() { override fun onScrolledToEnd() {
viewModel.loadNextPage() viewModel.loadNextPage()
} }
override fun onCreateActionMode( override fun onCreateActionMode(
controller: ListSelectionController, controller: ListSelectionController,
menuInflater: MenuInflater, menuInflater: MenuInflater,
menu: Menu menu: Menu
): Boolean { ): Boolean {
menuInflater.inflate(R.menu.mode_remote, menu) menuInflater.inflate(R.menu.mode_remote, menu)
return super.onCreateActionMode(controller, menuInflater, menu) return super.onCreateActionMode(controller, menuInflater, menu)
} }
override fun onFilterClick(view: View?) { override fun onFilterClick(view: View?) {
router.showFilterSheet() router.showFilterSheet()
} }
override fun onEmptyActionClick() { override fun onEmptyActionClick() {
if (filterCoordinator.isFilterApplied) { if (filterCoordinator.isFilterApplied) {
filterCoordinator.reset() filterCoordinator.reset()
} else { } else {
openInBrowser(null) // should never be called openInBrowser(null) // should never be called
} }
} }
override fun onFooterButtonClick() { override fun onFooterButtonClick() {
val filter = filterCoordinator.snapshot().listFilter val filter = filterCoordinator.snapshot().listFilter
when { when {
!filter.query.isNullOrEmpty() -> router.openSearch(filter.query.orEmpty(), SearchKind.SIMPLE) !filter.query.isNullOrEmpty() -> router.openSearch(filter.query.orEmpty(), SearchKind.SIMPLE)
!filter.author.isNullOrEmpty() -> router.openSearch(filter.author.orEmpty(), SearchKind.AUTHOR) !filter.author.isNullOrEmpty() -> router.openSearch(filter.author.orEmpty(), SearchKind.AUTHOR)
filter.tags.size == 1 -> router.openSearch(filter.tags.singleOrNull()?.title.orEmpty(), SearchKind.TAG) filter.tags.size == 1 -> router.openSearch(filter.tags.singleOrNull()?.title.orEmpty(), SearchKind.TAG)
} }
} }
override fun onSecondaryErrorActionClick(error: Throwable) { override fun onSecondaryErrorActionClick(error: Throwable) {
openInBrowser(error.getCauseUrl()) openInBrowser(error.getCauseUrl())
} }
private fun openInBrowser(url: String?) { override fun onClick(v: View?) = Unit // from Snackbar, do nothing
if (url?.isHttpUrl() == true) {
router.openBrowser(
url = url,
source = viewModel.source,
title = viewModel.source.getTitle(requireContext()),
)
} else {
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
.show()
}
}
private inner class RemoteListMenuProvider : MenuProvider { private fun openInBrowser(url: String?) {
if (url?.isHttpUrl() == true) {
router.openBrowser(
url = url,
source = viewModel.source,
title = viewModel.source.getTitle(requireContext()),
)
} else {
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
.show()
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { private fun showSourceBrokenWarning() {
menuInflater.inflate(R.menu.opt_list_remote, menu) val snackbar = Snackbar.make(
} viewBinding?.recyclerView ?: return,
R.string.source_broken_warning,
Snackbar.LENGTH_INDEFINITE,
)
snackbar.setAction(R.string.got_it, this)
snackbar.show()
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { private inner class RemoteListMenuProvider : MenuProvider {
R.id.action_source_settings -> {
router.openSourceSettings(viewModel.source)
true
}
R.id.action_random -> { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
viewModel.openRandom() menuInflater.inflate(R.menu.opt_list_remote, menu)
true }
}
R.id.action_filter -> { override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
onFilterClick(null) R.id.action_source_settings -> {
true router.openSourceSettings(viewModel.source)
} true
}
R.id.action_filter_reset -> { R.id.action_random -> {
filterCoordinator.reset() viewModel.openRandom()
true true
} }
else -> false R.id.action_filter -> {
} onFilterClick(null)
true
}
override fun onPrepareMenu(menu: Menu) { R.id.action_filter_reset -> {
super.onPrepareMenu(menu) filterCoordinator.reset()
menu.findItem(R.id.action_random)?.isEnabled = !viewModel.isRandomLoading.value true
menu.findItem(R.id.action_filter_reset)?.isVisible = filterCoordinator.isFilterApplied }
}
}
companion object { else -> false
}
const val ARG_SOURCE = "provider" override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_random)?.isEnabled = !viewModel.isRandomLoading.value
menu.findItem(R.id.action_filter_reset)?.isVisible = filterCoordinator.isFilterApplied
}
}
fun newInstance(source: MangaSource) = RemoteListFragment().withArgs(1) { companion object {
putString(ARG_SOURCE, source.name)
} const val ARG_SOURCE = "provider"
}
fun newInstance(source: MangaSource) = RemoteListFragment().withArgs(1) {
putString(ARG_SOURCE, source.name)
}
}
} }

View File

@@ -44,6 +44,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
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.MangaParserSource
import org.koitharu.kotatsu.parsers.util.sizeOrZero import org.koitharu.kotatsu.parsers.util.sizeOrZero
import javax.inject.Inject import javax.inject.Inject
@@ -65,6 +66,7 @@ open class RemoteListViewModel @Inject constructor(
val source = MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]) val source = MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE])
val isRandomLoading = MutableStateFlow(false) val isRandomLoading = MutableStateFlow(false)
val onOpenManga = MutableEventFlow<Manga>() val onOpenManga = MutableEventFlow<Manga>()
val onSourceBroken = MutableEventFlow<Unit>()
protected val repository = mangaRepositoryFactory.create(source) protected val repository = mangaRepositoryFactory.create(source)
private val mangaList = MutableStateFlow<List<Manga>?>(null) private val mangaList = MutableStateFlow<List<Manga>?>(null)
@@ -117,6 +119,11 @@ open class RemoteListViewModel @Inject constructor(
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
sourcesRepository.trackUsage(source) sourcesRepository.trackUsage(source)
} }
if (source is MangaParserSource && source.isBroken) {
// Just notify one. Will show reason in future
onSourceBroken.call(Unit)
}
} }
override fun onRefresh() { override fun onRefresh() {

View File

@@ -899,4 +899,5 @@
<string name="create_or_restore_backup">Create or restore a backup</string> <string name="create_or_restore_backup">Create or restore a backup</string>
<string name="data_removal">Data removal</string> <string name="data_removal">Data removal</string>
<string name="privacy">Privacy</string> <string name="privacy">Privacy</string>
<string name="source_broken_warning">This manga source has been marked as broken. Some features may not work</string>
</resources> </resources>