Quick filter for favorites
This commit is contained in:
@@ -17,6 +17,9 @@ class NetworkState(
|
|||||||
|
|
||||||
private val callback = NetworkCallbackImpl()
|
private val callback = NetworkCallbackImpl()
|
||||||
|
|
||||||
|
override val value: Boolean
|
||||||
|
get() = connectivityManager.isOnline(settings)
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
invalidate()
|
invalidate()
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ abstract class MediatorStateFlow<T>(initialValue: T) : StateFlow<T> {
|
|||||||
final override val replayCache: List<T>
|
final override val replayCache: List<T>
|
||||||
get() = delegate.replayCache
|
get() = delegate.replayCache
|
||||||
|
|
||||||
final override val value: T
|
override val value: T
|
||||||
get() = delegate.value
|
get() = delegate.value
|
||||||
|
|
||||||
final override suspend fun collect(collector: FlowCollector<T>): Nothing {
|
final override suspend fun collect(collector: FlowCollector<T>): Nothing {
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ 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.intellij.lang.annotations.Language
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.koitharu.kotatsu.core.db.entity.toEntity
|
||||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
import org.koitharu.kotatsu.list.domain.ListSortOrder
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@@ -27,21 +29,11 @@ abstract class FavouritesDao {
|
|||||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
|
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
|
||||||
abstract suspend fun findLast(limit: Int): List<FavouriteManga>
|
abstract suspend fun findLast(limit: Int): List<FavouriteManga>
|
||||||
|
|
||||||
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> {
|
fun observeAll(
|
||||||
val orderBy = getOrderBy(order)
|
order: ListSortOrder,
|
||||||
val query = buildString {
|
filterOptions: Set<ListFilterOption>,
|
||||||
append(
|
limit: Int
|
||||||
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
|
): Flow<List<FavouriteManga>> = observeAll(0L, order, filterOptions, limit)
|
||||||
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ",
|
|
||||||
)
|
|
||||||
append(orderBy)
|
|
||||||
if (limit > 0) {
|
|
||||||
append(" LIMIT ")
|
|
||||||
append(limit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return observeAllImpl(SimpleSQLiteQuery(query))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
||||||
@@ -57,13 +49,37 @@ abstract class FavouritesDao {
|
|||||||
)
|
)
|
||||||
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
|
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
|
||||||
|
|
||||||
fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<FavouriteManga>> {
|
fun observeAll(
|
||||||
|
categoryId: Long,
|
||||||
|
order: ListSortOrder,
|
||||||
|
filterOptions: Set<ListFilterOption>,
|
||||||
|
limit: Int
|
||||||
|
): Flow<List<FavouriteManga>> {
|
||||||
val orderBy = getOrderBy(order)
|
val orderBy = getOrderBy(order)
|
||||||
val query = buildString {
|
val query = buildString {
|
||||||
append(
|
append(
|
||||||
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
|
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
|
||||||
"WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY ",
|
"WHERE deleted_at = 0",
|
||||||
)
|
)
|
||||||
|
if (categoryId != 0L) {
|
||||||
|
append(" AND category_id = ")
|
||||||
|
append(categoryId)
|
||||||
|
}
|
||||||
|
val groupedOptions = filterOptions.groupBy { it.groupKey }
|
||||||
|
for ((_, group) in groupedOptions) {
|
||||||
|
if (group.isEmpty()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
append(" AND ")
|
||||||
|
if (group.size > 1) {
|
||||||
|
group.joinTo(this, separator = " OR ", prefix = "(", postfix = ")") {
|
||||||
|
it.getCondition()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(group.single().getCondition())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append(" GROUP BY favourites.manga_id ORDER BY ")
|
||||||
append(orderBy)
|
append(orderBy)
|
||||||
if (limit > 0) {
|
if (limit > 0) {
|
||||||
append(" LIMIT ")
|
append(" LIMIT ")
|
||||||
@@ -71,7 +87,7 @@ abstract class FavouritesDao {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return observeAllImpl(SimpleSQLiteQuery(query, arrayOf<Any>(categoryId)))
|
return observeAllImpl(SimpleSQLiteQuery(query))
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
|
suspend fun findCovers(categoryId: Long, order: ListSortOrder): List<Cover> {
|
||||||
@@ -191,4 +207,11 @@ abstract class FavouritesDao {
|
|||||||
|
|
||||||
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
else -> throw IllegalArgumentException("Sort order $sortOrder is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ListFilterOption.getCondition(): String = when (this) {
|
||||||
|
ListFilterOption.Macro.COMPLETED -> "EXISTS(SELECT * FROM history WHERE history.manga_id = favourites.manga_id AND history.percent >= 0.9999)"
|
||||||
|
ListFilterOption.Macro.NEW_CHAPTERS -> "(SELECT chapters_new FROM tracks WHERE tracks.manga_id = favourites.manga_id) > 0"
|
||||||
|
is ListFilterOption.Tag -> "EXISTS(SELECT * FROM manga_tags WHERE favourites.manga_id = manga_tags.manga_id AND tag_id = ${tag.toEntity().id})"
|
||||||
|
else -> throw IllegalArgumentException("Unsupported option $this")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.koitharu.kotatsu.favourites.domain
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
|
import org.koitharu.kotatsu.list.domain.MangaListQuickFilter
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FavoritesListQuickFilter @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val repository: FavouritesRepository,
|
||||||
|
networkState: NetworkState,
|
||||||
|
) : MangaListQuickFilter() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList {
|
||||||
|
add(ListFilterOption.Downloaded)
|
||||||
|
if (settings.isTrackerEnabled) {
|
||||||
|
add(ListFilterOption.Macro.NEW_CHAPTERS)
|
||||||
|
}
|
||||||
|
add(ListFilterOption.Macro.COMPLETED)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import org.koitharu.kotatsu.favourites.data.toFavouriteCategory
|
|||||||
import org.koitharu.kotatsu.favourites.data.toManga
|
import org.koitharu.kotatsu.favourites.data.toManga
|
||||||
import org.koitharu.kotatsu.favourites.data.toMangaList
|
import org.koitharu.kotatsu.favourites.data.toMangaList
|
||||||
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
import org.koitharu.kotatsu.favourites.domain.model.Cover
|
||||||
|
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.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -38,8 +39,8 @@ class FavouritesRepository @Inject constructor(
|
|||||||
return entities.toMangaList()
|
return entities.toMangaList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeAll(order: ListSortOrder, limit: Int): Flow<List<Manga>> {
|
fun observeAll(order: ListSortOrder, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
|
||||||
return db.getFavouritesDao().observeAll(order, limit)
|
return db.getFavouritesDao().observeAll(order, filterOptions, limit)
|
||||||
.mapItems { it.toManga() }
|
.mapItems { it.toManga() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,14 +49,19 @@ class FavouritesRepository @Inject constructor(
|
|||||||
return entities.toMangaList()
|
return entities.toMangaList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeAll(categoryId: Long, order: ListSortOrder, limit: Int): Flow<List<Manga>> {
|
fun observeAll(
|
||||||
return db.getFavouritesDao().observeAll(categoryId, order, limit)
|
categoryId: Long,
|
||||||
|
order: ListSortOrder,
|
||||||
|
filterOptions: Set<ListFilterOption>,
|
||||||
|
limit: Int
|
||||||
|
): Flow<List<Manga>> {
|
||||||
|
return db.getFavouritesDao().observeAll(categoryId, order, filterOptions, limit)
|
||||||
.mapItems { it.toManga() }
|
.mapItems { it.toManga() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeAll(categoryId: Long, limit: Int): Flow<List<Manga>> {
|
fun observeAll(categoryId: Long, filterOptions: Set<ListFilterOption>, limit: Int): Flow<List<Manga>> {
|
||||||
return observeOrder(categoryId)
|
return observeOrder(categoryId)
|
||||||
.flatMapLatest { order -> observeAll(categoryId, order, limit) }
|
.flatMapLatest { order -> observeAll(categoryId, order, filterOptions, limit) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeMangaCount(): Flow<Int> {
|
fun observeMangaCount(): Flow<Int> {
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class FavouritesListFragment : MangaListFragment(), PopupMenu.OnMenuItemClickLis
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
||||||
|
|
||||||
|
override fun onEmptyActionClick() = viewModel.clearFilter()
|
||||||
|
|
||||||
override fun onFilterClick(view: View?) {
|
override fun onFilterClick(view: View?) {
|
||||||
val menu = PopupMenu(view?.context ?: return, view)
|
val menu = PopupMenu(view?.context ?: return, view)
|
||||||
menu.setOnMenuItemClickListener(this)
|
menu.setOnMenuItemClickListener(this)
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import androidx.lifecycle.SavedStateHandle
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -15,21 +18,28 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
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.ui.util.ReversibleAction
|
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||||
import org.koitharu.kotatsu.core.util.ext.call
|
import org.koitharu.kotatsu.core.util.ext.call
|
||||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.FavoritesListQuickFilter
|
||||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
|
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.ARG_CATEGORY_ID
|
||||||
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
|
||||||
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
|
import org.koitharu.kotatsu.history.domain.MarkAsReadUseCase
|
||||||
|
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.domain.QuickFilterListener
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
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.toErrorState
|
import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalMangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -42,9 +52,11 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
private val repository: FavouritesRepository,
|
private val repository: FavouritesRepository,
|
||||||
private val mangaListMapper: MangaListMapper,
|
private val mangaListMapper: MangaListMapper,
|
||||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||||
|
private val quickFilter: FavoritesListQuickFilter,
|
||||||
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : MangaListViewModel(settings, downloadScheduler) {
|
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||||
|
|
||||||
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
|
val categoryId: Long = savedStateHandle[ARG_CATEGORY_ID] ?: NO_ID
|
||||||
private val refreshTrigger = MutableStateFlow(Any())
|
private val refreshTrigger = MutableStateFlow(Any())
|
||||||
@@ -66,26 +78,19 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
|
|
||||||
override val content = combine(
|
override val content = combine(
|
||||||
observeFavorites(),
|
observeFavorites(),
|
||||||
|
quickFilter.appliedOptions,
|
||||||
observeListModeWithTriggers(),
|
observeListModeWithTriggers(),
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
) { list, mode, _ ->
|
) { list, filters, mode, _ ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> listOf(
|
list.isEmpty() -> if (filters.isEmpty()) {
|
||||||
EmptyState(
|
listOf(getEmptyState(hasFilters = false))
|
||||||
icon = R.drawable.ic_empty_favourites,
|
|
||||||
textPrimary = R.string.text_empty_holder_primary,
|
|
||||||
textSecondary = if (categoryId == NO_ID) {
|
|
||||||
R.string.you_have_not_favourites_yet
|
|
||||||
} else {
|
} else {
|
||||||
R.string.favourites_category_empty
|
listOf(quickFilter.filterItem(filters), getEmptyState(hasFilters = true))
|
||||||
},
|
}
|
||||||
actionStringRes = 0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
isReady.set(true)
|
list.mapList(mode, filters).also { isReady.set(true) }
|
||||||
mangaListMapper.toListModelList(list, mode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.catch {
|
}.catch {
|
||||||
@@ -134,12 +139,55 @@ class FavouritesListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeFavorites() = if (categoryId == NO_ID) {
|
private suspend fun List<Manga>.mapList(mode: ListMode, filters: Set<ListFilterOption>): List<ListModel> {
|
||||||
combine(sortOrder.filterNotNull(), limit, ::Pair)
|
val list = if (ListFilterOption.Downloaded in filters) {
|
||||||
.flatMapLatest { repository.observeAll(it.first, it.second) }
|
mapToLocal()
|
||||||
} else {
|
} else {
|
||||||
limit.flatMapLatest {
|
this
|
||||||
repository.observeAll(categoryId, it)
|
}
|
||||||
|
val result = ArrayList<ListModel>(list.size + 1)
|
||||||
|
result += quickFilter.filterItem(filters)
|
||||||
|
mangaListMapper.toListModelList(result, list, mode)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeFavorites() = if (categoryId == NO_ID) {
|
||||||
|
combine(sortOrder.filterNotNull(), quickFilter.appliedOptions, limit, ::Triple)
|
||||||
|
.flatMapLatest { repository.observeAll(it.first, it.second - ListFilterOption.Downloaded, it.third) }
|
||||||
|
} else {
|
||||||
|
combine(quickFilter.appliedOptions, limit, ::Pair)
|
||||||
|
.flatMapLatest { repository.observeAll(categoryId, it.first - ListFilterOption.Downloaded, it.second) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEmptyState(hasFilters: Boolean) = if (hasFilters) {
|
||||||
|
EmptyState(
|
||||||
|
icon = R.drawable.ic_empty_favourites,
|
||||||
|
textPrimary = R.string.nothing_found,
|
||||||
|
textSecondary = R.string.text_empty_holder_secondary_filtered,
|
||||||
|
actionStringRes = R.string.reset_filter,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyState(
|
||||||
|
icon = R.drawable.ic_empty_favourites,
|
||||||
|
textPrimary = R.string.text_empty_holder_primary,
|
||||||
|
textSecondary = if (categoryId == NO_ID) {
|
||||||
|
R.string.you_have_not_favourites_yet
|
||||||
|
} else {
|
||||||
|
R.string.favourites_category_empty
|
||||||
|
},
|
||||||
|
actionStringRes = 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun List<Manga>.mapToLocal(): List<Manga> = coroutineScope {
|
||||||
|
map {
|
||||||
|
async {
|
||||||
|
if (it.isLocal) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
localMangaRepository.findSavedManga(it)?.manga
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.awaitAll().filterNotNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.history.domain
|
package org.koitharu.kotatsu.history.domain
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
import org.koitharu.kotatsu.list.domain.ListFilterOption
|
||||||
@@ -9,8 +10,13 @@ import javax.inject.Inject
|
|||||||
class HistoryListQuickFilter @Inject constructor(
|
class HistoryListQuickFilter @Inject constructor(
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val repository: HistoryRepository,
|
private val repository: HistoryRepository,
|
||||||
|
networkState: NetworkState,
|
||||||
) : MangaListQuickFilter() {
|
) : MangaListQuickFilter() {
|
||||||
|
|
||||||
|
init {
|
||||||
|
setFilterOption(ListFilterOption.Downloaded, !networkState.value)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList {
|
override suspend fun getAvailableFilterOptions(): List<ListFilterOption> = buildList {
|
||||||
add(ListFilterOption.Downloaded)
|
add(ListFilterOption.Downloaded)
|
||||||
if (settings.isTrackerEnabled) {
|
if (settings.isTrackerEnabled) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
import org.koitharu.kotatsu.core.os.NetworkManageIntent
|
|
||||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||||
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
|
import org.koitharu.kotatsu.core.ui.list.RecyclerScrollKeeper
|
||||||
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
import org.koitharu.kotatsu.core.ui.util.MenuInvalidator
|
||||||
@@ -34,9 +33,7 @@ class HistoryListFragment : MangaListFragment() {
|
|||||||
|
|
||||||
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
override fun onScrolledToEnd() = viewModel.requestMoreItems()
|
||||||
|
|
||||||
override fun onEmptyActionClick() {
|
override fun onEmptyActionClick() = viewModel.clearFilter()
|
||||||
startActivity(NetworkManageIntent())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
|
override fun onCreateActionMode(controller: ListSelectionController, mode: ActionMode, menu: Menu): Boolean {
|
||||||
mode.menuInflater.inflate(R.menu.mode_history, menu)
|
mode.menuInflater.inflate(R.menu.mode_history, menu)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import kotlinx.coroutines.plus
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||||
import org.koitharu.kotatsu.core.model.isLocal
|
import org.koitharu.kotatsu.core.model.isLocal
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||||
@@ -26,7 +25,6 @@ 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.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
|
||||||
@@ -38,7 +36,6 @@ 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.domain.QuickFilterListener
|
import org.koitharu.kotatsu.list.domain.QuickFilterListener
|
||||||
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
import org.koitharu.kotatsu.list.ui.MangaListViewModel
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyHint
|
|
||||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
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
|
||||||
@@ -61,7 +58,6 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val markAsReadUseCase: MarkAsReadUseCase,
|
private val markAsReadUseCase: MarkAsReadUseCase,
|
||||||
private val quickFilter: HistoryListQuickFilter,
|
private val quickFilter: HistoryListQuickFilter,
|
||||||
networkState: NetworkState,
|
|
||||||
downloadScheduler: DownloadWorker.Scheduler,
|
downloadScheduler: DownloadWorker.Scheduler,
|
||||||
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
) : MangaListViewModel(settings, downloadScheduler), QuickFilterListener by quickFilter {
|
||||||
|
|
||||||
@@ -98,9 +94,8 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
observeHistory(),
|
observeHistory(),
|
||||||
isGroupingEnabled,
|
isGroupingEnabled,
|
||||||
observeListModeWithTriggers(),
|
observeListModeWithTriggers(),
|
||||||
networkState,
|
|
||||||
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
|
||||||
) { filters, list, grouped, mode, online, incognito ->
|
) { filters, list, grouped, mode, incognito ->
|
||||||
when {
|
when {
|
||||||
list.isEmpty() -> {
|
list.isEmpty() -> {
|
||||||
if (filters.isEmpty()) {
|
if (filters.isEmpty()) {
|
||||||
@@ -112,7 +107,7 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
isReady.set(true)
|
isReady.set(true)
|
||||||
mapList(filters, list, grouped, mode, online, incognito)
|
mapList(list, grouped, mode, filters, incognito)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onStart {
|
}.onStart {
|
||||||
@@ -166,19 +161,18 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) }
|
.flatMapLatest { repository.observeAllWithHistory(it.first, it.second - ListFilterOption.Downloaded, it.third) }
|
||||||
|
|
||||||
private suspend fun mapList(
|
private suspend fun mapList(
|
||||||
filters: Set<ListFilterOption>,
|
|
||||||
historyList: List<MangaWithHistory>,
|
historyList: List<MangaWithHistory>,
|
||||||
grouped: Boolean,
|
grouped: Boolean,
|
||||||
mode: ListMode,
|
mode: ListMode,
|
||||||
isOnline: Boolean,
|
filters: Set<ListFilterOption>,
|
||||||
isIncognito: Boolean,
|
isIncognito: Boolean,
|
||||||
): List<ListModel> {
|
): List<ListModel> {
|
||||||
val list = if (!isOnline || ListFilterOption.Downloaded in filters) {
|
val list = if (ListFilterOption.Downloaded in filters) {
|
||||||
historyList.mapToLocal()
|
historyList.mapToLocal()
|
||||||
} else {
|
} else {
|
||||||
historyList
|
historyList
|
||||||
}
|
}
|
||||||
val result = ArrayList<ListModel>((if (grouped) (list.size * 1.4).toInt() else list.size) + 3)
|
val result = ArrayList<ListModel>((if (grouped) (list.size * 1.4).toInt() else list.size) + 2)
|
||||||
result += quickFilter.filterItem(filters)
|
result += quickFilter.filterItem(filters)
|
||||||
if (isIncognito) {
|
if (isIncognito) {
|
||||||
result += TipModel(
|
result += TipModel(
|
||||||
@@ -192,14 +186,6 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
val order = sortOrder.value
|
val order = sortOrder.value
|
||||||
var prevHeader: ListHeader? = null
|
var prevHeader: ListHeader? = null
|
||||||
if (!isOnline) {
|
|
||||||
result += EmptyHint(
|
|
||||||
icon = R.drawable.ic_empty_common,
|
|
||||||
textPrimary = R.string.network_unavailable,
|
|
||||||
textSecondary = R.string.network_unavailable_hint,
|
|
||||||
actionStringRes = R.string.manage,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
var isEmpty = true
|
var isEmpty = true
|
||||||
for ((manga, history) in list) {
|
for ((manga, history) in list) {
|
||||||
isEmpty = false
|
isEmpty = false
|
||||||
@@ -263,8 +249,8 @@ class HistoryListViewModel @Inject constructor(
|
|||||||
EmptyState(
|
EmptyState(
|
||||||
icon = R.drawable.ic_empty_history,
|
icon = R.drawable.ic_empty_history,
|
||||||
textPrimary = R.string.nothing_found,
|
textPrimary = R.string.nothing_found,
|
||||||
textSecondary = R.string.text_history_holder_secondary_filtered,
|
textSecondary = R.string.text_empty_holder_secondary_filtered,
|
||||||
actionStringRes = 0,
|
actionStringRes = R.string.reset_filter,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ abstract class MangaListQuickFilter : QuickFilterListener {
|
|||||||
val appliedOptions
|
val appliedOptions
|
||||||
get() = appliedFilter.asStateFlow()
|
get() = appliedFilter.asStateFlow()
|
||||||
|
|
||||||
|
override fun setFilterOption(option: ListFilterOption, isApplied: Boolean) {
|
||||||
|
appliedFilter.value = ArraySet(appliedFilter.value).also {
|
||||||
|
if (isApplied) {
|
||||||
|
it.add(option)
|
||||||
|
} else {
|
||||||
|
it.remove(option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun toggleFilterOption(option: ListFilterOption) {
|
override fun toggleFilterOption(option: ListFilterOption) {
|
||||||
appliedFilter.value = ArraySet(appliedFilter.value).also {
|
appliedFilter.value = ArraySet(appliedFilter.value).also {
|
||||||
if (option in it) {
|
if (option in it) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.list.domain
|
|||||||
|
|
||||||
interface QuickFilterListener {
|
interface QuickFilterListener {
|
||||||
|
|
||||||
|
fun setFilterOption(option: ListFilterOption, isApplied: Boolean)
|
||||||
|
|
||||||
fun toggleFilterOption(option: ListFilterOption)
|
fun toggleFilterOption(option: ListFilterOption)
|
||||||
|
|
||||||
fun clearFilter()
|
fun clearFilter()
|
||||||
|
|||||||
@@ -17,3 +17,12 @@ fun Throwable.toErrorState(canRetry: Boolean = true, @StringRes secondaryAction:
|
|||||||
fun Throwable.toErrorFooter() = ErrorFooter(
|
fun Throwable.toErrorFooter() = ErrorFooter(
|
||||||
exception = this,
|
exception = this,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
operator fun ListModel.plus(list: List<ListModel>): List<ListModel> {
|
||||||
|
val result = ArrayList<ListModel>(list.size + 1)
|
||||||
|
result.add(this)
|
||||||
|
result.addAll(list)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun ListModel.plus(other: ListModel): List<ListModel> = listOf(this, other)
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
android:id="@+id/scrollView"
|
android:id="@+id/scrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingHorizontal="@dimen/list_spacing"
|
android:paddingHorizontal="@dimen/list_spacing"
|
||||||
android:scrollbars="none">
|
android:scrollbars="none">
|
||||||
@@ -15,7 +16,8 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingVertical="2dp"
|
android:paddingTop="2dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
|
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter"
|
||||||
app:selectionRequired="false"
|
app:selectionRequired="false"
|
||||||
app:singleLine="true"
|
app:singleLine="true"
|
||||||
|
|||||||
@@ -96,7 +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_empty_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