From d0ed1fb85f9b2c78b1cef7c91da0887b4a236654 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 21 Oct 2025 18:04:05 +0300 Subject: [PATCH] Notify about broken source on list screen --- .../remotelist/ui/RemoteListFragment.kt | 245 +++++++++--------- .../remotelist/ui/RemoteListViewModel.kt | 7 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 137 insertions(+), 116 deletions(-) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt index 53ca229a2..11c570954 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListFragment.kt @@ -29,120 +29,133 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.search.domain.SearchKind @AndroidEntryPoint -class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner { - - override val viewModel by viewModels() - - override val filterCoordinator: FilterCoordinator - get() = viewModel.filterCoordinator - - override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { - super.onViewBindingCreated(binding, savedInstanceState) - addMenuProvider(RemoteListMenuProvider()) - addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel)) - viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity())) - viewModel.onOpenManga.observeEvent(viewLifecycleOwner) { router.openDetails(it) } - filterCoordinator.observe().distinctUntilChangedBy { it.listFilter.isEmpty() } - .drop(1) - .observe(viewLifecycleOwner) { - activity?.invalidateMenu() - } - } - - override fun onScrolledToEnd() { - viewModel.loadNextPage() - } - - override fun onCreateActionMode( - controller: ListSelectionController, - menuInflater: MenuInflater, - menu: Menu - ): Boolean { - menuInflater.inflate(R.menu.mode_remote, menu) - return super.onCreateActionMode(controller, menuInflater, menu) - } - - override fun onFilterClick(view: View?) { - router.showFilterSheet() - } - - override fun onEmptyActionClick() { - if (filterCoordinator.isFilterApplied) { - filterCoordinator.reset() - } else { - openInBrowser(null) // should never be called - } - } - - override fun onFooterButtonClick() { - val filter = filterCoordinator.snapshot().listFilter - when { - !filter.query.isNullOrEmpty() -> router.openSearch(filter.query.orEmpty(), SearchKind.SIMPLE) - !filter.author.isNullOrEmpty() -> router.openSearch(filter.author.orEmpty(), SearchKind.AUTHOR) - filter.tags.size == 1 -> router.openSearch(filter.tags.singleOrNull()?.title.orEmpty(), SearchKind.TAG) - } - } - - override fun onSecondaryErrorActionClick(error: Throwable) { - openInBrowser(error.getCauseUrl()) - } - - 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() - } - } - - private inner class RemoteListMenuProvider : MenuProvider { - - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.opt_list_remote, menu) - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { - R.id.action_source_settings -> { - router.openSourceSettings(viewModel.source) - true - } - - R.id.action_random -> { - viewModel.openRandom() - true - } - - R.id.action_filter -> { - onFilterClick(null) - true - } - - R.id.action_filter_reset -> { - filterCoordinator.reset() - true - } - - else -> false - } - - 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 - } - } - - companion object { - - const val ARG_SOURCE = "provider" - - fun newInstance(source: MangaSource) = RemoteListFragment().withArgs(1) { - putString(ARG_SOURCE, source.name) - } - } +class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner, View.OnClickListener { + + override val viewModel by viewModels() + + override val filterCoordinator: FilterCoordinator + get() = viewModel.filterCoordinator + + override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) { + super.onViewBindingCreated(binding, savedInstanceState) + addMenuProvider(RemoteListMenuProvider()) + addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel)) + viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity())) + viewModel.onOpenManga.observeEvent(viewLifecycleOwner) { router.openDetails(it) } + viewModel.onSourceBroken.observeEvent(viewLifecycleOwner) { showSourceBrokenWarning() } + filterCoordinator.observe().distinctUntilChangedBy { it.listFilter.isEmpty() } + .drop(1) + .observe(viewLifecycleOwner) { + activity?.invalidateMenu() + } + } + + override fun onScrolledToEnd() { + viewModel.loadNextPage() + } + + override fun onCreateActionMode( + controller: ListSelectionController, + menuInflater: MenuInflater, + menu: Menu + ): Boolean { + menuInflater.inflate(R.menu.mode_remote, menu) + return super.onCreateActionMode(controller, menuInflater, menu) + } + + override fun onFilterClick(view: View?) { + router.showFilterSheet() + } + + override fun onEmptyActionClick() { + if (filterCoordinator.isFilterApplied) { + filterCoordinator.reset() + } else { + openInBrowser(null) // should never be called + } + } + + override fun onFooterButtonClick() { + val filter = filterCoordinator.snapshot().listFilter + when { + !filter.query.isNullOrEmpty() -> router.openSearch(filter.query.orEmpty(), SearchKind.SIMPLE) + !filter.author.isNullOrEmpty() -> router.openSearch(filter.author.orEmpty(), SearchKind.AUTHOR) + filter.tags.size == 1 -> router.openSearch(filter.tags.singleOrNull()?.title.orEmpty(), SearchKind.TAG) + } + } + + override fun onSecondaryErrorActionClick(error: Throwable) { + openInBrowser(error.getCauseUrl()) + } + + override fun onClick(v: View?) = Unit // from Snackbar, do nothing + + 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() + } + } + + private fun showSourceBrokenWarning() { + val snackbar = Snackbar.make( + viewBinding?.recyclerView ?: return, + R.string.source_broken_warning, + Snackbar.LENGTH_INDEFINITE, + ) + snackbar.setAction(R.string.got_it, this) + snackbar.show() + } + + private inner class RemoteListMenuProvider : MenuProvider { + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.opt_list_remote, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { + R.id.action_source_settings -> { + router.openSourceSettings(viewModel.source) + true + } + + R.id.action_random -> { + viewModel.openRandom() + true + } + + R.id.action_filter -> { + onFilterClick(null) + true + } + + R.id.action_filter_reset -> { + filterCoordinator.reset() + true + } + + else -> false + } + + 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 + } + } + + companion object { + + const val ARG_SOURCE = "provider" + + fun newInstance(source: MangaSource) = RemoteListFragment().withArgs(1) { + putString(ARG_SOURCE, source.name) + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index ea6b513ff..3896dde8c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -44,6 +44,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.util.sizeOrZero import javax.inject.Inject @@ -65,6 +66,7 @@ open class RemoteListViewModel @Inject constructor( val source = MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]) val isRandomLoading = MutableStateFlow(false) val onOpenManga = MutableEventFlow() + val onSourceBroken = MutableEventFlow() protected val repository = mangaRepositoryFactory.create(source) private val mangaList = MutableStateFlow?>(null) @@ -117,6 +119,11 @@ open class RemoteListViewModel @Inject constructor( launchJob(Dispatchers.Default) { sourcesRepository.trackUsage(source) } + + if (source is MangaParserSource && source.isBroken) { + // Just notify one. Will show reason in future + onSourceBroken.call(Unit) + } } override fun onRefresh() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd2c1ad6d..ee0b9e5c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -899,4 +899,5 @@ Create or restore a backup Data removal Privacy + This manga source has been marked as broken. Some features may not work