diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt index 12ff405f5..236dbd091 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/nav/AppRouter.kt @@ -575,7 +575,7 @@ class AppRouter private constructor( /** Public utils **/ fun isFilterSupported(): Boolean = when { - fragment != null -> fragment.activity is FilterCoordinator.Owner + fragment != null -> FilterCoordinator.find(fragment) != null activity != null -> activity is FilterCoordinator.Owner else -> false } @@ -812,6 +812,7 @@ class AppRouter private constructor( const val KEY_FILTER = "filter" const val KEY_ID = "id" const val KEY_INDEX = "index" + const val KEY_IS_BOTTOMTAB = "is_btab" const val KEY_KIND = "kind" const val KEY_LIST_SECTION = "list_section" const val KEY_MANGA = "manga" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt index 03f923016..c42cbc52e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/FilterCoordinator.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.filter.ui +import androidx.fragment.app.Fragment import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.ViewModelLifecycle import dagger.hilt.android.scopes.ViewModelScoped @@ -489,9 +490,27 @@ class FilterCoordinator @Inject constructor( val filterCoordinator: FilterCoordinator } - private companion object { + companion object { - const val TAGS_LIMIT = 12 - val MAX_YEAR = Calendar.getInstance()[Calendar.YEAR] + 1 - } + private const val TAGS_LIMIT = 12 + private val MAX_YEAR = Calendar.getInstance()[Calendar.YEAR] + 1 + + fun find(fragment: Fragment): FilterCoordinator? { + (fragment.activity as? Owner)?.let { + return it.filterCoordinator + } + var f = fragment + while (true) { + (f as? Owner)?.let { + return it.filterCoordinator + } + f = f.parentFragment ?: break + } + return null + } + + fun require(fragment: Fragment): FilterCoordinator { + return find(fragment) ?: throw IllegalStateException("FilterCoordinator cannot be found") + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt index 77c8e0c6a..921f7c593 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/sheet/FilterSheetFragment.kt @@ -55,7 +55,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), binding.scrollView.scrollIndicators = 0 } } - val filter = requireFilter() + val filter = FilterCoordinator.require(this) filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged) filter.locale.observe(viewLifecycleOwner, this::onLocaleChanged) filter.originalLocale.observe(viewLifecycleOwner, this::onOriginalLocaleChanged) @@ -103,7 +103,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), } override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { - val filter = requireFilter() + val filter = FilterCoordinator.require(this) when (parent.id) { R.id.spinner_order -> filter.setSortOrder(filter.sortOrder.value.availableItems[position]) R.id.spinner_locale -> filter.setLocale(filter.locale.value.availableItems[position]) @@ -118,7 +118,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), return } val intValue = value.toInt() - val filter = requireFilter() + val filter = FilterCoordinator.require(this) when (slider.id) { R.id.slider_year -> filter.setYear( if (intValue <= slider.valueFrom.toIntUp()) { @@ -134,7 +134,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), if (!fromUser) { return } - val filter = requireFilter() + val filter = FilterCoordinator.require(this) when (slider.id) { R.id.slider_yearsRange -> filter.setYearRange( valueFrom = slider.values.firstOrNull()?.let { @@ -148,7 +148,7 @@ class FilterSheetFragment : BaseAdaptiveSheet(), } override fun onChipClick(chip: Chip, data: Any?) { - val filter = requireFilter() + val filter = FilterCoordinator.require(this) when (data) { is MangaState -> filter.toggleState(data, !chip.isChecked) is MangaTag -> if (chip.parentView?.id == R.id.chips_genresExclude) { @@ -356,6 +356,4 @@ class FilterSheetFragment : BaseAdaptiveSheet(), ) b.sliderYearsRange.setValuesRounded(currentValueFrom, currentValueTo) } - - private fun requireFilter() = (requireActivity() as FilterCoordinator.Owner).filterCoordinator } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogSheet.kt index 398b748a1..54fb12fbd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/filter/ui/tags/TagsCatalogSheet.kt @@ -36,7 +36,7 @@ class TagsCatalogSheet : BaseAdaptiveSheet(), extrasProducer = { defaultViewModelCreationExtras.withCreationCallback { factory -> factory.create( - filter = (requireActivity() as FilterCoordinator.Owner).filterCoordinator, + filter = FilterCoordinator.require(this), isExcludeTag = requireArguments().getBoolean(AppRouter.KEY_EXCLUDE), ) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/CoverImageView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/CoverImageView.kt index 6ab8c74e6..056bd40d9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/CoverImageView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/image/ui/CoverImageView.kt @@ -189,7 +189,7 @@ class CoverImageView @JvmOverloads constructor( override fun onError(request: ImageRequest, result: ErrorResult) { super.onError(request, result) - foreground = if (result.throwable.isNetworkError()) { + foreground = if (result.throwable.isNetworkError() && !networkState.isOnline()) { ContextCompat.getDrawable(context, R.drawable.ic_offline)?.let { LayerDrawable(arrayOf(it)).apply { setLayerGravity(0, Gravity.CENTER) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt index 5ebc837cb..121acff41 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/preview/PreviewFragment.kt @@ -74,7 +74,7 @@ class PreviewFragment : BaseFragment(), View.OnClickList override fun onChipClick(chip: Chip, data: Any?) { val tag = data as? MangaTag ?: return - val filter = (activity as? FilterCoordinator.Owner)?.filterCoordinator + val filter = FilterCoordinator.find(this) if (filter == null) { router.openList(tag) } else { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt index d781020d6..286263971 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListFragment.kt @@ -23,7 +23,6 @@ import org.koitharu.kotatsu.core.util.ShareHelper import org.koitharu.kotatsu.core.util.ext.addMenuProvider import org.koitharu.kotatsu.core.util.ext.observeEvent import org.koitharu.kotatsu.core.util.ext.tryLaunch -import org.koitharu.kotatsu.core.util.ext.withArgs import org.koitharu.kotatsu.databinding.FragmentListBinding import org.koitharu.kotatsu.filter.ui.FilterCoordinator import org.koitharu.kotatsu.list.ui.MangaListFragment @@ -45,13 +44,14 @@ class LocalListFragment : MangaListFragment(), FilterCoordinator.Owner { } } - init { - withArgs(1) { - putString( - RemoteListFragment.ARG_SOURCE, - LocalMangaSource.name, - ) // required by FilterCoordinator - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val args = arguments ?: Bundle(1) + args.putString( + RemoteListFragment.ARG_SOURCE, + LocalMangaSource.name, + ) // required by FilterCoordinator + arguments = args } override val viewModel by viewModels() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index cbffb2a7b..4a15c156c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -6,10 +6,13 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharedFlow import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.model.toChipModel +import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.toFileOrNull @@ -17,10 +20,13 @@ import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.explore.data.MangaSourcesRepository import org.koitharu.kotatsu.explore.domain.ExploreRepository import org.koitharu.kotatsu.filter.ui.FilterCoordinator +import org.koitharu.kotatsu.list.domain.ListFilterOption import org.koitharu.kotatsu.list.domain.MangaListMapper +import org.koitharu.kotatsu.list.domain.QuickFilterListener import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.MangaListModel +import org.koitharu.kotatsu.list.ui.model.QuickFilter import org.koitharu.kotatsu.list.ui.model.TipModel import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageManager @@ -52,9 +58,10 @@ class LocalListViewModel @Inject constructor( exploreRepository = exploreRepository, sourcesRepository = sourcesRepository, mangaDataRepository = mangaDataRepository, -), SharedPreferences.OnSharedPreferenceChangeListener { +), SharedPreferences.OnSharedPreferenceChangeListener, QuickFilterListener { val onMangaRemoved = MutableEventFlow() + private val showInlineFilter: Boolean = savedStateHandle[AppRouter.KEY_IS_BOTTOMTAB] ?: false init { launchJob(Dispatchers.Default) { @@ -68,29 +75,49 @@ class LocalListViewModel @Inject constructor( override suspend fun onBuildList(list: MutableList) { super.onBuildList(list) - if (localStorageManager.hasExternalStoragePermission(isReadOnly = true)) { - return - } - for (item in list) { - if (item !is MangaListModel) { - continue + if (showInlineFilter) { + createFilterHeader(maxCount = 16)?.let { + list.add(0, it) } - val file = item.manga.url.toUriOrNull()?.toFileOrNull() ?: continue - if (localStorageManager.isOnExternalStorage(file)) { - val tip = TipModel( - key = "permission", - title = R.string.external_storage, - text = R.string.missing_storage_permission, - icon = R.drawable.ic_storage, - primaryButtonText = R.string.fix, - secondaryButtonText = R.string.settings, - ) - list.add(0, tip) - return + } + if (!localStorageManager.hasExternalStoragePermission(isReadOnly = true)) { + for (item in list) { + if (item !is MangaListModel) { + continue + } + val file = item.manga.url.toUriOrNull()?.toFileOrNull() ?: continue + if (localStorageManager.isOnExternalStorage(file)) { + val tip = TipModel( + key = "permission", + title = R.string.external_storage, + text = R.string.missing_storage_permission, + icon = R.drawable.ic_storage, + primaryButtonText = R.string.fix, + secondaryButtonText = R.string.settings, + ) + list.add(0, tip) + return + } } } } + override fun setFilterOption(option: ListFilterOption, isApplied: Boolean) { + if (option is ListFilterOption.Tag) { + filterCoordinator.toggleTag(option.tag, isApplied) + } + } + + override fun toggleFilterOption(option: ListFilterOption) { + if (option is ListFilterOption.Tag) { + val tag = option.tag + val isSelected = tag in filterCoordinator.snapshot().listFilter.tags + filterCoordinator.toggleTag(option.tag, !isSelected) + } + } + + override fun clearFilter() = filterCoordinator.reset() + override fun onCleared() { settings.unsubscribe(this) super.onCleared() @@ -125,4 +152,26 @@ class LocalListViewModel @Inject constructor( actionStringRes = R.string._import, ) } + + private suspend fun createFilterHeader(maxCount: Int): QuickFilter? { + val appliedTags = filterCoordinator.snapshot().listFilter.tags + val availableTags = repository.getFilterOptions().availableTags + if (appliedTags.isEmpty() && availableTags.size < 3) { + return null + } + val result = ArrayList(minOf(availableTags.size, maxCount)) + appliedTags.mapTo(result) { tag -> + ListFilterOption.Tag(tag).toChipModel(isChecked = true) + } + for (tag in availableTags) { + if (result.size >= maxCount) { + break + } + if (tag in appliedTags) { + continue + } + result.add(ListFilterOption.Tag(tag).toChipModel(isChecked = false)) + } + return QuickFilter(result) + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt index 4d39b7918..d5cb08d8b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt @@ -25,10 +25,12 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.R import org.koitharu.kotatsu.bookmarks.ui.AllBookmarksFragment +import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.NavItem import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView +import org.koitharu.kotatsu.core.util.ext.buildBundle import org.koitharu.kotatsu.core.util.ext.setContentDescriptionAndTooltip import org.koitharu.kotatsu.core.util.ext.smoothScrollToTop import org.koitharu.kotatsu.databinding.NavigationRailFabBinding @@ -211,10 +213,13 @@ class MainNavigationDelegate( return false } val fragment = instantiateFragment(fragmentClass) + val args = buildBundle(1) { + putBoolean(AppRouter.KEY_IS_BOTTOMTAB, true) + } fragment.enterTransition = MaterialFadeThrough() fragmentManager.beginTransaction() .setReorderingAllowed(true) - .replace(R.id.container, fragmentClass, null, TAG_PRIMARY) + .replace(R.id.container, fragmentClass, args, TAG_PRIMARY) .runOnCommit { onFragmentChanged(fragment, fromUser = true) } .commit() return true 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 f09d5cec9..f072bc87d 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 @@ -62,7 +62,7 @@ open class RemoteListViewModel @Inject constructor( val isRandomLoading = MutableStateFlow(false) val onOpenManga = MutableEventFlow() - private val repository = mangaRepositoryFactory.create(source) + protected val repository = mangaRepositoryFactory.create(source) private val mangaList = MutableStateFlow?>(null) private val hasNextPage = MutableStateFlow(false) private val listError = MutableStateFlow(null)