Quick filter in history draft implementation
This commit is contained in:
@@ -5,6 +5,7 @@ import android.util.AttributeSet
|
|||||||
import android.view.View.OnClickListener
|
import android.view.View.OnClickListener
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.view.children
|
import androidx.core.view.children
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.chip.ChipDrawable
|
import com.google.android.material.chip.ChipDrawable
|
||||||
@@ -92,7 +93,11 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun bindChip(chip: Chip, model: ChipModel) {
|
private fun bindChip(chip: Chip, model: ChipModel) {
|
||||||
chip.text = model.title
|
if (model.titleResId == 0) {
|
||||||
|
chip.text = model.title
|
||||||
|
} else {
|
||||||
|
chip.setText(model.titleResId)
|
||||||
|
}
|
||||||
chip.isClickable = onChipClickListener != null || model.isCheckable
|
chip.isClickable = onChipClickListener != null || model.isCheckable
|
||||||
chip.isCheckable = model.isCheckable
|
chip.isCheckable = model.isCheckable
|
||||||
if (model.icon == 0) {
|
if (model.icon == 0) {
|
||||||
@@ -139,7 +144,8 @@ class ChipsView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class ChipModel(
|
data class ChipModel(
|
||||||
val title: CharSequence,
|
val title: CharSequence? = null,
|
||||||
|
@StringRes val titleResId: Int = 0,
|
||||||
@DrawableRes val icon: Int = 0,
|
@DrawableRes val icon: Int = 0,
|
||||||
val isCheckable: Boolean = false,
|
val isCheckable: Boolean = false,
|
||||||
@ColorRes val tint: Int = 0,
|
@ColorRes val tint: Int = 0,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery
|
|||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -27,7 +28,11 @@ abstract class HistoryDao {
|
|||||||
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
|
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC LIMIT :limit")
|
||||||
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
|
abstract fun observeAll(limit: Int): Flow<List<HistoryWithManga>>
|
||||||
|
|
||||||
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<HistoryWithManga>> {
|
fun observeAll(
|
||||||
|
order: ListSortOrder,
|
||||||
|
filterOptions: Set<ListFilterOption>,
|
||||||
|
limit: Int
|
||||||
|
): Flow<List<HistoryWithManga>> {
|
||||||
val orderBy = when (order) {
|
val orderBy = when (order) {
|
||||||
ListSortOrder.LAST_READ -> "history.updated_at DESC"
|
ListSortOrder.LAST_READ -> "history.updated_at DESC"
|
||||||
ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC"
|
ListSortOrder.LONG_AGO_READ -> "history.updated_at ASC"
|
||||||
@@ -44,8 +49,13 @@ abstract class HistoryDao {
|
|||||||
val query = buildString {
|
val query = buildString {
|
||||||
append(
|
append(
|
||||||
"SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " +
|
"SELECT * FROM history LEFT JOIN manga ON history.manga_id = manga.manga_id " +
|
||||||
"WHERE history.deleted_at = 0 GROUP BY history.manga_id ORDER BY ",
|
"WHERE history.deleted_at = 0",
|
||||||
)
|
)
|
||||||
|
for (option in filterOptions) {
|
||||||
|
append(" AND ")
|
||||||
|
append(option.getCondition())
|
||||||
|
}
|
||||||
|
append(" GROUP BY history.manga_id ORDER BY ")
|
||||||
append(orderBy)
|
append(orderBy)
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
append(" LIMIT ")
|
append(" LIMIT ")
|
||||||
@@ -147,4 +157,11 @@ abstract class HistoryDao {
|
|||||||
@Transaction
|
@Transaction
|
||||||
@RawQuery(observedEntities = [HistoryEntity::class])
|
@RawQuery(observedEntities = [HistoryEntity::class])
|
||||||
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>>
|
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<HistoryWithManga>>
|
||||||
|
|
||||||
|
private fun ListFilterOption.getCondition(): String = when (this) {
|
||||||
|
ListFilterOption.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = history.manga_id) > 0"
|
||||||
|
ListFilterOption.FAVORITE -> "EXISTS(SELECT * FROM favourites WHERE history.manga_id = favourites.manga_id)"
|
||||||
|
ListFilterOption.COMPLETED -> "percent >= 0.9999"
|
||||||
|
ListFilterOption.DOWNLOADED -> throw IllegalArgumentException("Unsupported option $this")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
|
|||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
|
||||||
import org.koitharu.kotatsu.core.util.ext.mapItems
|
import org.koitharu.kotatsu.core.util.ext.mapItems
|
||||||
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
import org.koitharu.kotatsu.list.domain.ReadingProgress
|
import org.koitharu.kotatsu.list.domain.ReadingProgress
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
@@ -76,8 +77,12 @@ class HistoryRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeAllWithHistory(order: ListSortOrder, limit: Int): Flow<List<MangaWithHistory>> {
|
fun observeAllWithHistory(
|
||||||
return db.getHistoryDao().observeAll(order, limit).mapItems {
|
order: ListSortOrder,
|
||||||
|
filterOptions: Set<ListFilterOption>,
|
||||||
|
limit: Int
|
||||||
|
): Flow<List<MangaWithHistory>> {
|
||||||
|
return db.getHistoryDao().observeAll(order, filterOptions, limit).mapItems {
|
||||||
MangaWithHistory(
|
MangaWithHistory(
|
||||||
it.manga.toManga(it.tags.toMangaTags()),
|
it.manga.toManga(it.tags.toMangaTags()),
|
||||||
it.history.toMangaHistory(),
|
it.history.toMangaHistory(),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
|||||||
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
|
||||||
import org.koitharu.kotatsu.core.util.ext.observe
|
import org.koitharu.kotatsu.core.util.ext.observe
|
||||||
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
import org.koitharu.kotatsu.databinding.FragmentListBinding
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||||
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
import org.koitharu.kotatsu.list.ui.size.DynamicItemSizeResolver
|
||||||
|
|
||||||
@@ -34,6 +35,10 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
||||||
|
|
||||||
|
override fun onFilterOptionClick(option: ListFilterOption) {
|
||||||
|
viewModel.onFilterOptionClick(option)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEmptyActionClick() {
|
override fun onEmptyActionClick() {
|
||||||
startActivity(NetworkManageIntent())
|
startActivity(NetworkManageIntent())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ import org.koitharu.kotatsu.core.prefs.ListMode
|
|||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
|
||||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
|
import org.koitharu.kotatsu.core.util.ext.calculateTimeAgo
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.combine
|
||||||
import org.koitharu.kotatsu.core.util.ext.onFirst
|
import org.koitharu.kotatsu.core.util.ext.onFirst
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
|
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
|
||||||
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
import org.koitharu.kotatsu.list.domain.MangaListMapper
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
@@ -36,11 +39,13 @@ import org.koitharu.kotatsu.list.ui.model.EmptyState
|
|||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.QuickFilter
|
||||||
import org.koitharu.kotatsu.list.ui.model.TipModel
|
import org.koitharu.kotatsu.list.ui.model.TipModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.util.EnumSet
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -63,6 +68,8 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
valueProducer = { historySortOrder },
|
valueProducer = { historySortOrder },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val filterOptions = MutableStateFlow<Set<ListFilterOption>>(EnumSet.noneOf(ListFilterOption::class.java))
|
||||||
|
|
||||||
override val listMode = settings.observeAsStateFlow(
|
override val listMode = settings.observeAsStateFlow(
|
||||||
scope = viewModelScope + Dispatchers.Default,
|
scope = viewModelScope + Dispatchers.Default,
|
||||||
key = AppSettings.KEY_LIST_MODE_HISTORY,
|
key = AppSettings.KEY_LIST_MODE_HISTORY,
|
||||||
@@ -86,25 +93,25 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
|
filterOptions,
|
||||||
observeHistory(),
|
observeHistory(),
|
||||||
isGroupingEnabled,
|
isGroupingEnabled,
|
||||||
observeListModeWithTriggers(),
|
observeListModeWithTriggers(),
|
||||||
networkState,
|
networkState,
|
||||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
||||||
) { list, grouped, mode, online, incognito ->
|
) { filters, list, grouped, mode, online, incognito ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> {
|
||||||
EmptyState(
|
if (filters.isEmpty()) {
|
||||||
icon = R.drawable.ic_empty_history,
|
listOf(getEmptyState(hasFilters = false))
|
||||||
textPrimary = R.string.text_history_holder_primary,
|
} else {
|
||||||
textSecondary = R.string.text_history_holder_secondary,
|
listOf(filterItem(filters), getEmptyState(hasFilters = true))
|
||||||
actionStringRes = 0,
|
}
|
||||||
),
|
}
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
isReady.set(true)
|
isReady.set(true)
|
||||||
mapList(list, grouped, mode, online, incognito)
|
mapList(filters, list, grouped, mode, online, incognito)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onStart {
|
}.onStart {
|
||||||
@@ -154,17 +161,29 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeHistory() = combine(sortOrder, limit, ::Pair)
|
fun onFilterOptionClick(option: ListFilterOption) {
|
||||||
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second) }
|
filterOptions.value = EnumSet.copyOf(filterOptions.value).also {
|
||||||
|
if (option in it) {
|
||||||
|
it.remove(option)
|
||||||
|
} else {
|
||||||
|
it.add(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeHistory() = combine(sortOrder, filterOptions, limit, ::Triple)
|
||||||
|
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.DOWNLOADED, it.third) }
|
||||||
|
|
||||||
private suspend fun mapList(
|
private suspend fun mapList(
|
||||||
|
filters: Set<ListFilterOption>,
|
||||||
list: List<MangaWithHistory>,
|
list: List<MangaWithHistory>,
|
||||||
grouped: Boolean,
|
grouped: Boolean,
|
||||||
mode: ListMode,
|
mode: ListMode,
|
||||||
isOnline: Boolean,
|
isOnline: Boolean,
|
||||||
isIncognito: Boolean,
|
isIncognito: Boolean,
|
||||||
): List<ListModel> {
|
): List<ListModel> {
|
||||||
val result = ArrayList<ListModel>(if (grouped) (list.size * 1.4).toInt() else list.size + 2)
|
val result = ArrayList<ListModel>((if (grouped) (list.size * 1.4).toInt() else list.size) + 3)
|
||||||
|
result += filterItem(filters)
|
||||||
if (isIncognito) {
|
if (isIncognito) {
|
||||||
result += TipModel(
|
result += TipModel(
|
||||||
key = AppSettings.KEY_INCOGNITO_MODE,
|
key = AppSettings.KEY_INCOGNITO_MODE,
|
||||||
@@ -185,12 +204,14 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
actionStringRes = R.string.manage,
|
actionStringRes = R.string.manage,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
var isEmpty = true
|
||||||
for ((m, history) in list) {
|
for ((m, history) in list) {
|
||||||
val manga = if (!isOnline && !m.isLocal) {
|
val manga = if ((!isOnline && !m.isLocal) || ListFilterOption.DOWNLOADED in filters) {
|
||||||
localMangaRepository.findSavedManga(m)?.manga ?: continue
|
localMangaRepository.findSavedManga(m)?.manga ?: continue
|
||||||
} else {
|
} else {
|
||||||
m
|
m
|
||||||
}
|
}
|
||||||
|
isEmpty = false
|
||||||
if (grouped) {
|
if (grouped) {
|
||||||
val header = history.header(order)
|
val header = history.header(order)
|
||||||
if (header != prevHeader) {
|
if (header != prevHeader) {
|
||||||
@@ -202,6 +223,9 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
result += mangaListMapper.toListModel(manga, mode)
|
result += mangaListMapper.toListModel(manga, mode)
|
||||||
}
|
}
|
||||||
|
if (filters.isNotEmpty() && isEmpty) {
|
||||||
|
result += getEmptyState(hasFilters = true)
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,4 +253,32 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
ListSortOrder.UPDATED,
|
ListSortOrder.UPDATED,
|
||||||
ListSortOrder.RATING -> null
|
ListSortOrder.RATING -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun filterItem(selected: Set<ListFilterOption>) = QuickFilter(
|
||||||
|
items = ListFilterOption.HISTORY.map { option ->
|
||||||
|
ChipsView.ChipModel(
|
||||||
|
titleResId = option.titleResId,
|
||||||
|
icon = option.iconResId,
|
||||||
|
isCheckable = true,
|
||||||
|
isChecked = option in selected,
|
||||||
|
data = option,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
|
||||||
|
EmptyState(
|
||||||
|
icon = R.drawable.ic_empty_history,
|
||||||
|
textPrimary = R.string.nothing_found,
|
||||||
|
textSecondary = R.string.text_history_holder_secondary_filtered,
|
||||||
|
actionStringRes = 0,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyState(
|
||||||
|
icon = R.drawable.ic_empty_history,
|
||||||
|
textPrimary = R.string.text_history_holder_primary,
|
||||||
|
textSecondary = R.string.text_history_holder_secondary,
|
||||||
|
actionStringRes = 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package org.koitharu.kotatsu.list.domain
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import java.util.EnumSet
|
||||||
|
|
||||||
|
enum class ListFilterOption(
|
||||||
|
@StringRes val titleResId: Int,
|
||||||
|
@DrawableRes val iconResId: Int,
|
||||||
|
) {
|
||||||
|
|
||||||
|
DOWNLOADED(R.string.on_device, R.drawable.ic_storage),
|
||||||
|
COMPLETED(R.string.status_completed, R.drawable.ic_state_finished),
|
||||||
|
NEW_CHAPTERS(R.string.new_chapters, R.drawable.ic_updated),
|
||||||
|
FAVORITE(R.string.favourites, R.drawable.ic_heart_outline),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val HISTORY: Set<ListFilterOption> = EnumSet.of(
|
||||||
|
DOWNLOADED,
|
||||||
|
NEW_CHAPTERS,
|
||||||
|
FAVORITE,
|
||||||
|
COMPLETED,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ import org.koitharu.kotatsu.databinding.FragmentListBinding
|
|||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
@@ -226,6 +227,8 @@ abstract class MangaListFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFilterOptionClick(option: ListFilterOption) = Unit
|
||||||
|
|
||||||
override fun onFilterClick(view: View?) = Unit
|
override fun onFilterClick(view: View?) = Unit
|
||||||
|
|
||||||
override fun onEmptyActionClick() = Unit
|
override fun onEmptyActionClick() = Unit
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.list.ui.adapter
|
|||||||
|
|
||||||
enum class ListItemType {
|
enum class ListItemType {
|
||||||
|
|
||||||
|
FILTER_HEADER,
|
||||||
FILTER_SORT,
|
FILTER_SORT,
|
||||||
FILTER_TAG,
|
FILTER_TAG,
|
||||||
FILTER_TAG_MULTI,
|
FILTER_TAG_MULTI,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ open class MangaListAdapter(
|
|||||||
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listener))
|
||||||
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
|
addDelegate(ListItemType.HINT_EMPTY, emptyHintAD(coil, lifecycleOwner, listener))
|
||||||
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
|
addDelegate(ListItemType.HEADER, listHeaderAD(listener))
|
||||||
|
addDelegate(ListItemType.FILTER_HEADER, quickFilterAD(listener))
|
||||||
addDelegate(ListItemType.TIP, tipAD(listener))
|
addDelegate(ListItemType.TIP, tipAD(listener))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ fun mangaListDetailedItemAD(
|
|||||||
source(item.source)
|
source(item.source)
|
||||||
enqueueWith(coil)
|
enqueueWith(coil)
|
||||||
}
|
}
|
||||||
binding.textViewTags.text = item.tags.joinToString(separator = ", ") { it.title }
|
binding.textViewTags.text = item.tags.joinToString(separator = ", ") { it.title ?: "" }
|
||||||
badge = itemView.bindBadge(badge, item.counter)
|
badge = itemView.bindBadge(badge, item.counter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.core.ui.widgets.TipView
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
|
||||||
interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener,
|
interface MangaListListener : MangaDetailsClickListener, ListStateHolderListener, ListHeaderClickListener,
|
||||||
TipView.OnButtonClickListener {
|
TipView.OnButtonClickListener, QuickFilterClickListener {
|
||||||
|
|
||||||
fun onUpdateFilter(tags: Set<MangaTag>)
|
fun onUpdateFilter(tags: Set<MangaTag>)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemQuickFilterBinding
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.QuickFilter
|
||||||
|
|
||||||
|
fun quickFilterAD(
|
||||||
|
listener: QuickFilterClickListener,
|
||||||
|
) = adapterDelegateViewBinding<QuickFilter, ListModel, ItemQuickFilterBinding>(
|
||||||
|
{ layoutInflater, parent -> ItemQuickFilterBinding.inflate(layoutInflater, parent, false) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
binding.chipsTags.onChipClickListener = ChipsView.OnChipClickListener { chip, data ->
|
||||||
|
if (data is ListFilterOption) {
|
||||||
|
listener.onFilterOptionClick(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bind {
|
||||||
|
binding.chipsTags.setChips(item.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.adapter
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
|
|
||||||
|
interface QuickFilterClickListener {
|
||||||
|
|
||||||
|
fun onFilterOptionClick(option: ListFilterOption)
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ class TypedListSpacingDecoration(
|
|||||||
ListItemType.FILTER_TAG_MULTI,
|
ListItemType.FILTER_TAG_MULTI,
|
||||||
ListItemType.FILTER_STATE,
|
ListItemType.FILTER_STATE,
|
||||||
ListItemType.FILTER_LANGUAGE,
|
ListItemType.FILTER_LANGUAGE,
|
||||||
|
ListItemType.FILTER_HEADER,
|
||||||
-> outRect.set(0)
|
-> outRect.set(0)
|
||||||
|
|
||||||
ListItemType.HEADER,
|
ListItemType.HEADER,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||||
|
|
||||||
|
data class QuickFilter(
|
||||||
|
val items: List<ChipsView.ChipModel>,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean = other is QuickFilter
|
||||||
|
|
||||||
|
override fun getChangePayload(previousState: ListModel) = ListModelDiffCallback.PAYLOAD_NESTED_LIST_CHANGED
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import org.koitharu.kotatsu.databinding.ActivitySearchMultiBinding
|
|||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
import org.koitharu.kotatsu.download.ui.worker.DownloadStartedObserver
|
||||||
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
import org.koitharu.kotatsu.favourites.ui.categories.select.FavoriteSheet
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||||
@@ -133,6 +134,8 @@ class MultiSearchActivity :
|
|||||||
viewModel.retry()
|
viewModel.retry()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFilterOptionClick(option: ListFilterOption) = Unit
|
||||||
|
|
||||||
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
||||||
|
|
||||||
override fun onFilterClick(view: View?) = Unit
|
override fun onFilterClick(view: View?) = Unit
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.util.ext.observe
|
|||||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||||
import org.koitharu.kotatsu.databinding.FragmentFeedBinding
|
import org.koitharu.kotatsu.databinding.FragmentFeedBinding
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
|
||||||
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
import org.koitharu.kotatsu.list.ui.adapter.TypedListSpacingDecoration
|
||||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||||
@@ -94,6 +95,8 @@ class FeedFragment :
|
|||||||
viewModel.update()
|
viewModel.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onFilterOptionClick(option: ListFilterOption) = Unit
|
||||||
|
|
||||||
override fun onRetryClick(error: Throwable) = Unit
|
override fun onRetryClick(error: Throwable) = Unit
|
||||||
|
|
||||||
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
override fun onUpdateFilter(tags: Set<MangaTag>) = Unit
|
||||||
|
|||||||
24
app/src/main/res/layout/item_quick_filter.xml
Normal file
24
app/src/main/res/layout/item_quick_filter.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<HorizontalScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/scrollView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingHorizontal="@dimen/list_spacing"
|
||||||
|
android:scrollbars="none">
|
||||||
|
|
||||||
|
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
|
android:id="@+id/chips_tags"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
|
||||||
|
app:selectionRequired="false"
|
||||||
|
app:singleLine="true"
|
||||||
|
app:singleSelection="false" />
|
||||||
|
|
||||||
|
</HorizontalScrollView>
|
||||||
@@ -96,6 +96,7 @@
|
|||||||
<string name="text_search_holder_secondary">Try to reformulate the query.</string>
|
<string name="text_search_holder_secondary">Try to reformulate the query.</string>
|
||||||
<string name="text_history_holder_primary">What you read will be displayed here</string>
|
<string name="text_history_holder_primary">What you read will be displayed here</string>
|
||||||
<string name="text_history_holder_secondary">Find what to read in the «Explore» section</string>
|
<string name="text_history_holder_secondary">Find what to read in the «Explore» section</string>
|
||||||
|
<string name="text_history_holder_secondary_filtered">There are no manga matching the filters you selected</string>
|
||||||
<string name="text_local_holder_primary">Save something first</string>
|
<string name="text_local_holder_primary">Save something first</string>
|
||||||
<string name="text_local_holder_secondary">Save something from an online catalog or import it from a file.</string>
|
<string name="text_local_holder_secondary">Save something from an online catalog or import it from a file.</string>
|
||||||
<string name="manga_shelf">Shelf</string>
|
<string name="manga_shelf">Shelf</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user