Filter for Local storage tab on main screen
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
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<SheetFilterBinding>(),
|
||||
}
|
||||
|
||||
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<SheetFilterBinding>(),
|
||||
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<SheetFilterBinding>(),
|
||||
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<SheetFilterBinding>(),
|
||||
}
|
||||
|
||||
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<SheetFilterBinding>(),
|
||||
)
|
||||
b.sliderYearsRange.setValuesRounded(currentValueFrom, currentValueTo)
|
||||
}
|
||||
|
||||
private fun requireFilter() = (requireActivity() as FilterCoordinator.Owner).filterCoordinator
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class TagsCatalogSheet : BaseAdaptiveSheet<SheetTagsBinding>(),
|
||||
extrasProducer = {
|
||||
defaultViewModelCreationExtras.withCreationCallback<TagsCatalogViewModel.Factory> { factory ->
|
||||
factory.create(
|
||||
filter = (requireActivity() as FilterCoordinator.Owner).filterCoordinator,
|
||||
filter = FilterCoordinator.require(this),
|
||||
isExcludeTag = requireArguments().getBoolean(AppRouter.KEY_EXCLUDE),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -74,7 +74,7 @@ class PreviewFragment : BaseFragment<FragmentPreviewBinding>(), 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 {
|
||||
|
||||
@@ -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<LocalListViewModel>()
|
||||
|
||||
@@ -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<Unit>()
|
||||
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<ListModel>) {
|
||||
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<ChipsView.ChipModel>(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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -62,7 +62,7 @@ open class RemoteListViewModel @Inject constructor(
|
||||
val isRandomLoading = MutableStateFlow(false)
|
||||
val onOpenManga = MutableEventFlow<Manga>()
|
||||
|
||||
private val repository = mangaRepositoryFactory.create(source)
|
||||
protected val repository = mangaRepositoryFactory.create(source)
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
private val hasNextPage = MutableStateFlow(false)
|
||||
private val listError = MutableStateFlow<Throwable?>(null)
|
||||
|
||||
Reference in New Issue
Block a user