Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,17 @@ class SavedFiltersRepository
val rewritten =
runCatching {
val filter = json.decodeFromString<PersistableFilter>(v)
val newSource = mangaSourceFromStoredKey(newSourceKey)
json.encodeToString(
filter.copy(source = mangaSourceFromStoredKey(newSourceKey)),
filter.copy(
source = newSource,
filter =
filter.filter.copy(
tags = filter.filter.tags.map { it.copy(source = newSource) }.toSet(),
tagsExclude =
filter.filter.tagsExclude.map { it.copy(source = newSource) }.toSet(),
),
),
)
}.getOrElse { v }
putString(k, rewritten)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ class FilterCoordinator
) { available, applied ->
FilterProperty(
availableItems = available,
selectedItems = setOfNotNull(available.find { it.filter == applied }),
selectedItems = setOfNotNull(available.find { it.matchesAppliedFilter(applied) }),
)
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.EMPTY)

Expand Down Expand Up @@ -403,10 +403,12 @@ class FilterCoordinator
fun applyDynamicFilters(
working: FilterList,
defaults: FilterList,
) {
): MangaListFilter {
val tags = FilterMapper.encode(working, defaults, repository.source)
val query = listFilter.value.takeQueryIfSupported()
listFilter.value = MangaListFilter(query = query, tags = tags)
val applied = MangaListFilter(query = query, tags = tags)
listFilter.value = applied
return applied
}

/** Loads the current sort state for the compact sort picker (the source's own sort, or the built-in orders). */
Expand Down Expand Up @@ -511,9 +513,35 @@ class FilterCoordinator
set(newFilter)
}

/**
* Applies a saved preset. Native filters are applied as-is; dynamic source presets keep the
* current source sort, since sort is a separate setting surfaced outside the filter sheet.
*/
fun applySavedFilter(preset: PersistableFilter) {
var filter = preset.filter.normalizeTagSources()
if (isDynamicFilter) {
val currentSortTag = listFilter.value.tags.firstOrNull { it.key.startsWith(FilterMapper.SORT_KEY_PREFIX) }
filter =
filter.copy(
tags = (filter.tags.withoutSortTags() + listOfNotNull(currentSortTag)).toSet(),
)
}
setAdjusted(filter)
}

/** True when the current filter has state worth saving (a dynamic sort alone is not). */
fun isSaveableFilter(value: MangaListFilter = listFilter.value): Boolean =
(if (isDynamicFilter) value.copy(tags = value.tags.withoutSortTags()) else value).isNotEmpty()

fun saveCurrentFilter(name: String) =
coroutineScope.launch {
savedFiltersRepository.save(repository.source, name, listFilter.value)
val filter =
if (isDynamicFilter) {
listFilter.value.copy(tags = listFilter.value.tags.withoutSortTags())
} else {
listFilter.value
}
savedFiltersRepository.save(repository.source, name, filter)
}

fun renameSavedFilter(
Expand All @@ -528,6 +556,24 @@ class FilterCoordinator
savedFiltersRepository.delete(repository.source, id)
}

private fun PersistableFilter.matchesAppliedFilter(applied: MangaListFilter): Boolean {
if (!isDynamicFilter) return filter == applied
val savedTags = filter.tags.withoutSortTags().map { it.key }.toSet()
val appliedTags = applied.tags.withoutSortTags().map { it.key }.toSet()
return savedTags == appliedTags &&
filter.copy(tags = emptySet(), tagsExclude = emptySet()) ==
applied.copy(tags = emptySet(), tagsExclude = emptySet())
}

private fun MangaListFilter.normalizeTagSources(): MangaListFilter =
copy(
tags = tags.map { it.copy(source = repository.source) }.toSet(),
tagsExclude = tagsExclude.map { it.copy(source = repository.source) }.toSet(),
)

private fun Set<MangaTag>.withoutSortTags(): Set<MangaTag> =
filterNot { it.key.startsWith(FilterMapper.SORT_KEY_PREFIX) }.toSet()

fun setQuery(value: String?) {
val newQuery = value?.trim()?.nullIfEmpty()
listFilter.update { oldValue ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class FilterHeaderFragment :
if (chip.isChecked) {
filter.reset()
} else {
filter.setAdjusted(data.filter)
filter.applySavedFilter(data)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import org.draken.usagi.core.ui.BaseViewModel
import org.draken.usagi.filter.data.PersistableFilter
import org.draken.usagi.filter.ui.FilterCoordinator
import org.draken.usagi.filter.ui.external.model.FilterItem
import tsuki.model.MangaListFilter

@HiltViewModel(assistedFactory = FilterViewModel.Factory::class)
class FilterViewModel
Expand All @@ -30,6 +36,7 @@ class FilterViewModel
private var sortPath: String? = null
private val pathMap = HashMap<String, Filter<*>>()
private val expandedPaths = HashSet<String>()
private var lastAppliedFilter: MangaListFilter? = null

private val itemsFlow = MutableStateFlow<List<FilterItem>>(emptyList())
private val loadedFlow = MutableStateFlow(false)
Expand All @@ -42,7 +49,16 @@ class FilterViewModel
}.stateIn(viewModelScope, SharingStarted.Eagerly, false)

init {
lastAppliedFilter = filter.snapshot().listFilter
load(resetExpanded = true)
filter.observe()
.map { it.listFilter }
.distinctUntilChanged()
.onEach { applied ->
if (applied != lastAppliedFilter) {
load(resetExpanded = true)
}
}.launchIn(viewModelScope)
}

private fun load(resetExpanded: Boolean) =
Expand All @@ -61,6 +77,13 @@ class FilterViewModel

fun reset() {
filter.reset()
lastAppliedFilter = filter.snapshot().listFilter
load(resetExpanded = true)
}

fun applySavedFilter(preset: PersistableFilter) {
filter.applySavedFilter(preset)
lastAppliedFilter = filter.snapshot().listFilter
load(resetExpanded = true)
}

Expand Down Expand Up @@ -126,7 +149,7 @@ class FilterViewModel
}

private fun apply() {
filter.applyDynamicFilters(working, defaults)
lastAppliedFilter = filter.applyDynamicFilters(working, defaults)
rebuild()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.chip.Chip
import com.google.android.material.shape.MaterialShapeDrawable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.withCreationCallback
import org.draken.usagi.R
Expand All @@ -23,19 +29,26 @@ import org.draken.usagi.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_EXPA
import org.draken.usagi.core.ui.sheet.AdaptiveSheetBehavior.Companion.STATE_SETTLING
import org.draken.usagi.core.ui.sheet.AdaptiveSheetCallback
import org.draken.usagi.core.ui.sheet.BaseAdaptiveSheet
import org.draken.usagi.core.ui.widgets.ChipsView
import org.draken.usagi.core.util.ext.consume
import org.draken.usagi.core.util.ext.getThemeColor
import org.draken.usagi.core.util.ext.observe
import org.draken.usagi.databinding.SheetOptionsBinding
import org.draken.usagi.filter.data.PersistableFilter
import org.draken.usagi.filter.ui.FilterCoordinator
import org.draken.usagi.filter.ui.external.FilterAdapter
import org.draken.usagi.filter.ui.external.FilterViewModel
import org.draken.usagi.filter.ui.model.FilterProperty
import org.draken.usagi.filter.ui.saved.SavedFilterActions
import com.google.android.material.R as materialR

@AndroidEntryPoint
class FilterSheetFragment :
BaseAdaptiveSheet<SheetOptionsBinding>(),
AdaptiveSheetCallback {
AdaptiveSheetCallback,
ChipsView.OnChipClickListener,
ChipsView.OnChipLongClickListener,
ChipsView.OnChipCloseClickListener {
private val viewModel by viewModels<FilterViewModel>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<FilterViewModel.Factory> { factory ->
Expand All @@ -44,6 +57,7 @@ class FilterSheetFragment :
},
)

private lateinit var savedFilterActions: SavedFilterActions
private var systemBarsBottom = 0

override fun onCreateViewBinding(
Expand All @@ -57,10 +71,29 @@ class FilterSheetFragment :
) {
super.onViewBindingCreated(binding, savedInstanceState)
if (dialog == null) binding.adjustForEmbeddedLayout()
val coordinator = FilterCoordinator.require(this)
savedFilterActions = SavedFilterActions(requireContext(), coordinator)
val adapter = FilterAdapter(viewModel)
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
binding.buttonReset.setOnClickListener { viewModel.reset() }
binding.buttonSave.setOnClickListener {
// Commit any pending text input before capturing the current filter state.
binding.recyclerView.findFocus()?.clearFocus()
savedFilterActions.showSaveDialog()
}
binding.chipsSavedFilters.onChipClickListener = this
binding.chipsSavedFilters.onChipLongClickListener = this
binding.chipsSavedFilters.onChipCloseClickListener = this
coordinator.savedFilters.observe(viewLifecycleOwner, ::onSavedFiltersChanged)
combine(
coordinator.observe().map { coordinator.isSaveableFilter(it.listFilter) }.distinctUntilChanged(),
coordinator.savedFilters.map { it.selectedItems.isEmpty() }.distinctUntilChanged(),
Boolean::and,
).flowOn(Dispatchers.Default)
.observe(viewLifecycleOwner) {
binding.buttonSave.isEnabled = it
}
binding.buttonDone.setOnClickListener {
// Commit any pending text input before closing: dismissing the sheet tears the
// view down immediately, so the field's focus-loss callback never runs and the
Expand Down Expand Up @@ -134,6 +167,64 @@ class FilterSheetFragment :
width = LinearLayout.LayoutParams.WRAP_CONTENT
gravity = Gravity.END or Gravity.CENTER_VERTICAL
}
buttonSave.updateLayoutParams<LinearLayout.LayoutParams> {
weight = 0f
width = LinearLayout.LayoutParams.WRAP_CONTENT
gravity = Gravity.END or Gravity.CENTER_VERTICAL
}
}

private fun onSavedFiltersChanged(value: FilterProperty<PersistableFilter>) {
val b = viewBinding ?: return
b.layoutSavedFilters.isVisible = !value.isEmpty()
if (value.isEmpty()) {
b.chipsSavedFilters.setChips(emptyList())
return
}
b.chipsSavedFilters.setChips(
value.availableItems.map { f ->
ChipsView.ChipModel(
title = f.name,
isChecked = f in value.selectedItems,
data = f,
isDropdown = true,
)
},
)
}

override fun onChipClick(
chip: Chip,
data: Any?,
) {
if (data is PersistableFilter) {
viewBinding?.recyclerView?.findFocus()?.clearFocus()
viewModel.applySavedFilter(data)
}
}

override fun onChipLongClick(
chip: Chip,
data: Any?,
): Boolean =
when (data) {
is PersistableFilter -> {
savedFilterActions.showMenu(chip, data)
true
}

else -> {
false
}
}

override fun onChipCloseClick(
chip: Chip,
data: Any?,
) {
if (data is PersistableFilter) {
savedFilterActions.showMenu(chip, data)
}
}

override fun onApplyWindowInsets(
Expand Down
Loading
Loading