From 2a5300a634b7c62d11a820e2f1c76ef2ddd9538e Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 18 Jan 2024 16:16:12 +0200 Subject: [PATCH] Show all favorites on categories screen --- .../kotatsu/favourites/data/FavouritesDao.kt | 22 +++- .../favourites/domain/FavouritesRepository.kt | 12 +- .../categories/FavouriteCategoriesActivity.kt | 16 ++- .../FavouriteCategoriesListListener.kt | 4 +- .../FavouritesCategoriesViewModel.kt | 69 ++++++++--- .../adapter/AllCategoriesListModel.kt | 15 +++ .../categories/adapter/CategoriesAdapter.kt | 1 + .../ui/categories/adapter/CategoryAD.kt | 66 +++++++++++ .../main/res/layout/item_categories_all.xml | 111 ++++++++++++++++++ 9 files changed, 290 insertions(+), 26 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesListModel.kt create mode 100644 app/src/main/res/layout/item_categories_all.xml diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt index c4d8e8ed4..ba7c02b18 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/data/FavouritesDao.kt @@ -82,19 +82,35 @@ abstract class FavouritesDao { ) abstract suspend fun findAllManga(categoryId: Int): List - suspend fun findCovers(categoryId: Long, order: ListSortOrder): List { + suspend fun findCovers(categoryId: Long, order: ListSortOrder, limit: Int): List { val orderBy = getOrderBy(order) @Language("RoomSql") val query = SimpleSQLiteQuery( "SELECT manga.cover_url AS url, manga.source AS source FROM favourites " + "LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + - "WHERE favourites.category_id = ? AND deleted_at = 0 ORDER BY $orderBy", - arrayOf(categoryId), + "WHERE favourites.category_id = ? AND deleted_at = 0 ORDER BY $orderBy LIMIT ?", + arrayOf(categoryId, limit), ) return findCoversImpl(query) } + suspend fun findCovers(order: ListSortOrder, limit: Int): List { + val orderBy = getOrderBy(order) + + @Language("RoomSql") + val query = SimpleSQLiteQuery( + "SELECT manga.cover_url AS url, manga.source AS source FROM favourites " + + "LEFT JOIN manga ON favourites.manga_id = manga.manga_id " + + "WHERE deleted_at = 0 GROUP BY manga.manga_id ORDER BY $orderBy LIMIT ?", + arrayOf(limit), + ) + return findCoversImpl(query) + } + + @Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0") + abstract fun observeMangaCount(): Flow + @Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)") abstract suspend fun findAllManga(): List diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt index 5b5fa094f..b3e4ec4f4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/domain/FavouritesRepository.kt @@ -60,6 +60,11 @@ class FavouritesRepository @Inject constructor( .flatMapLatest { order -> observeAll(categoryId, order) } } + fun observeMangaCount(): Flow { + return db.getFavouritesDao().observeMangaCount() + .distinctUntilChanged() + } + fun observeCategories(): Flow> { return db.getFavouriteCategoriesDao().observeAll().mapItems { it.toFavouriteCategory() @@ -72,7 +77,7 @@ class FavouritesRepository @Inject constructor( }.distinctUntilChanged() } - fun observeCategoriesWithCovers(): Flow>> { + fun observeCategoriesWithCovers(coversLimit: Int): Flow>> { return db.getFavouriteCategoriesDao().observeAll() .map { db.withTransaction { @@ -82,6 +87,7 @@ class FavouritesRepository @Inject constructor( res[cat] = db.getFavouritesDao().findCovers( categoryId = cat.id, order = cat.order, + limit = coversLimit, ) } res @@ -89,6 +95,10 @@ class FavouritesRepository @Inject constructor( } } + suspend fun getAllFavoritesCovers(order: ListSortOrder, limit: Int): List { + return db.getFavouritesDao().findCovers(order, limit) + } + fun observeCategory(id: Long): Flow { return db.getFavouriteCategoriesDao().observe(id) .map { it?.toFavouriteCategory() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt index 5e1f0982f..534092ebd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesActivity.kt @@ -76,7 +76,13 @@ class FavouriteCategoriesActivity : } } - override fun onItemClick(item: FavouriteCategory, view: View) { + override fun onItemClick(item: FavouriteCategory?, view: View) { + if (item == null) { + if (selectionController.count == 0) { + startActivity(FavouritesActivity.newIntent(view.context)) + } + return + } if (selectionController.onItemClick(item.id)) { return } @@ -92,8 +98,12 @@ class FavouriteCategoriesActivity : startActivity(intent) } - override fun onItemLongClick(item: FavouriteCategory, view: View): Boolean { - return selectionController.onItemLongClick(item.id) + override fun onItemLongClick(item: FavouriteCategory?, view: View): Boolean { + return item != null && selectionController.onItemLongClick(item.id) + } + + override fun onShowAllClick(isChecked: Boolean) { + viewModel.setAllCategoriesVisible(isChecked) } override fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt index e778d42f4..736a8f766 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouriteCategoriesListListener.kt @@ -5,9 +5,11 @@ import androidx.recyclerview.widget.RecyclerView import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.ui.list.OnListItemClickListener -interface FavouriteCategoriesListListener : OnListItemClickListener { +interface FavouriteCategoriesListListener : OnListItemClickListener { fun onDragHandleTouch(holder: RecyclerView.ViewHolder): Boolean fun onEditClick(item: FavouriteCategory, view: View) + + fun onShowAllClick(isChecked: Boolean) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt index 27fa877ee..4ba9720dc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/FavouritesCategoriesViewModel.kt @@ -5,17 +5,21 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.model.Cover +import org.koitharu.kotatsu.favourites.ui.categories.adapter.AllCategoriesListModel import org.koitharu.kotatsu.favourites.ui.categories.adapter.CategoryListModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.ListModel @@ -30,9 +34,13 @@ class FavouritesCategoriesViewModel @Inject constructor( private var commitJob: Job? = null - val content = repository.observeCategoriesWithCovers() - .map { it.toUiList() } - .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) + val content = combine( + repository.observeCategoriesWithCovers(coversLimit = 3), + observeAllCategories(), + settings.observeAsFlow(AppSettings.KEY_ALL_FAVOURITES_VISIBLE) { isAllFavouritesVisible }, + ) { cats, all, showAll -> + cats.toUiList(all, showAll) + }.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, listOf(LoadingState)) fun deleteCategories(ids: Set) { launchJob(Dispatchers.Default) { @@ -74,21 +82,46 @@ class FavouritesCategoriesViewModel @Inject constructor( } } - private fun Map>.toUiList(): List = map { (category, covers) -> - CategoryListModel( - mangaCount = covers.size, - covers = covers.take(3), - category = category, - isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources, - ) - }.ifEmpty { - listOf( - EmptyState( - icon = R.drawable.ic_empty_favourites, - textPrimary = R.string.text_empty_holder_primary, - textSecondary = R.string.empty_favourite_categories, - actionStringRes = 0, + private fun Map>.toUiList( + allFavorites: Pair>, + showAll: Boolean + ): List { + if (isEmpty()) { + return listOf( + EmptyState( + icon = R.drawable.ic_empty_favourites, + textPrimary = R.string.text_empty_holder_primary, + textSecondary = R.string.empty_favourite_categories, + actionStringRes = 0, + ), + ) + } + val result = ArrayList(size + 1) + result.add( + AllCategoriesListModel( + mangaCount = allFavorites.first, + covers = allFavorites.second, + isVisible = showAll, ), ) + mapTo(result) { (category, covers) -> + CategoryListModel( + mangaCount = covers.size, + covers = covers.take(3), + category = category, + isTrackerEnabled = settings.isTrackerEnabled && AppSettings.TRACK_FAVOURITES in settings.trackSources, + ) + } + return result + } + + private fun observeAllCategories(): Flow>> { + return settings.observeAsFlow(AppSettings.KEY_FAVORITES_ORDER) { + allFavoritesSortOrder + }.mapLatest { order -> + repository.getAllFavoritesCovers(order, limit = 3) + }.combine(repository.observeMangaCount()) { covers, count -> + count to covers + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesListModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesListModel.kt new file mode 100644 index 000000000..c3f5cd47b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/AllCategoriesListModel.kt @@ -0,0 +1,15 @@ +package org.koitharu.kotatsu.favourites.ui.categories.adapter + +import org.koitharu.kotatsu.favourites.domain.model.Cover +import org.koitharu.kotatsu.list.ui.model.ListModel + +data class AllCategoriesListModel( + val mangaCount: Int, + val covers: List, + val isVisible: Boolean, +) : ListModel { + + override fun areItemsTheSame(other: ListModel): Boolean { + return other is AllCategoriesListModel + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt index 43658d3a3..8569744cd 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoriesAdapter.kt @@ -19,6 +19,7 @@ class CategoriesAdapter( init { addDelegate(ListItemType.CATEGORY_LARGE, categoryAD(coil, lifecycleOwner, onItemClickListener)) + addDelegate(ListItemType.NAV_ITEM, allCategoriesAD(coil, lifecycleOwner, onItemClickListener)) addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, listListener)) addDelegate(ListItemType.STATE_LOADING, loadingStateAD()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt index 253827e88..1e0ae79b7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/favourites/ui/categories/adapter/CategoryAD.kt @@ -21,6 +21,7 @@ import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getThemeColor import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.source +import org.koitharu.kotatsu.databinding.ItemCategoriesAllBinding import org.koitharu.kotatsu.databinding.ItemCategoryBinding import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.list.ui.model.ListModel @@ -92,3 +93,68 @@ fun categoryAD( } } } + +fun allCategoriesAD( + coil: ImageLoader, + lifecycleOwner: LifecycleOwner, + clickListener: FavouriteCategoriesListListener, +) = adapterDelegateViewBinding( + { inflater, parent -> ItemCategoriesAllBinding.inflate(inflater, parent, false) }, +) { + val eventListener = OnClickListener { v -> + if (v.id == R.id.imageView_visible) { + clickListener.onShowAllClick(!item.isVisible) + } else { + clickListener.onItemClick(null, v) + } + } + val backgroundColor = context.getThemeColor(android.R.attr.colorBackground) + ImageViewCompat.setImageTintList( + binding.imageViewCover3, + ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)), + ) + ImageViewCompat.setImageTintList( + binding.imageViewCover2, + ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)), + ) + binding.imageViewCover2.backgroundTintList = + ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 76)) + binding.imageViewCover3.backgroundTintList = + ColorStateList.valueOf(ColorUtils.setAlphaComponent(backgroundColor, 153)) + val fallback = ColorDrawable(Color.TRANSPARENT) + val coverViews = arrayOf(binding.imageViewCover1, binding.imageViewCover2, binding.imageViewCover3) + val crossFadeDuration = context.getAnimationDuration(R.integer.config_defaultAnimTime).toInt() + itemView.setOnClickListener(eventListener) + binding.imageViewVisible.setOnClickListener(eventListener) + + bind { + binding.textViewSubtitle.text = if (item.mangaCount == 0) { + getString(R.string.empty) + } else { + context.resources.getQuantityString( + R.plurals.items, + item.mangaCount, + item.mangaCount, + ) + } + binding.imageViewVisible.setImageResource( + if (item.isVisible) { + R.drawable.ic_eye + } else { + R.drawable.ic_eye_off + }, + ) + repeat(coverViews.size) { i -> + val cover = item.covers.getOrNull(i) + coverViews[i].newImageRequest(lifecycleOwner, cover?.url)?.run { + placeholder(R.drawable.ic_placeholder) + fallback(fallback) + source(cover?.mangaSource) + crossfade(crossFadeDuration * (i + 1)) + error(R.drawable.ic_error_placeholder) + allowRgb565(true) + enqueueWith(coil) + } + } + } +} diff --git a/app/src/main/res/layout/item_categories_all.xml b/app/src/main/res/layout/item_categories_all.xml new file mode 100644 index 000000000..f7825e4c7 --- /dev/null +++ b/app/src/main/res/layout/item_categories_all.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + +