diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt index d6f7fc571..4bd188966 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/dao/TracksDao.kt @@ -13,6 +13,9 @@ abstract class TracksDao { @Query("SELECT * FROM tracks WHERE manga_id = :mangaId") abstract suspend fun find(mangaId: Long): TrackEntity? + @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") + abstract suspend fun findNewChapters(mangaId: Long): Int? + @Query("DELETE FROM tracks") abstract suspend fun clear() diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt index 4db6c0c27..9cd6e751e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/FavouritesModule.kt @@ -13,7 +13,7 @@ val favouritesModule single { FavouritesRepository(get()) } viewModel { categoryId -> - FavouritesListViewModel(categoryId.get(), get(), get()) + FavouritesListViewModel(categoryId.get(), get(), get(), get()) } viewModel { FavouritesCategoriesViewModel(get()) } viewModel { manga -> diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index 344c3d5a1..bfd8292ae 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -9,18 +9,21 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.SortOrder import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.favourites.domain.FavouritesRepository +import org.koitharu.kotatsu.list.domain.CountersProvider import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.toErrorState import org.koitharu.kotatsu.list.ui.model.toUi +import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct class FavouritesListViewModel( private val categoryId: Long, private val repository: FavouritesRepository, - settings: AppSettings -) : MangaListViewModel(settings) { + private val trackingRepository: TrackingRepository, + settings: AppSettings, +) : MangaListViewModel(settings), CountersProvider { override val content = combine( if (categoryId == 0L) { @@ -42,7 +45,7 @@ class FavouritesListViewModel( } ) ) - else -> list.toUi(mode) + else -> list.toUi(mode, this) } }.catch { emit(listOf(it.toErrorState(canRetry = false))) @@ -61,4 +64,8 @@ class FavouritesListViewModel( } } } + + override suspend fun getCounter(mangaId: Long): Int { + return trackingRepository.getNewChaptersCount(mangaId) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt index bf6ea6304..74fb0ef40 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -9,5 +9,5 @@ val historyModule get() = module { single { HistoryRepository(get(), get(), get()) } - viewModel { HistoryListViewModel(get(), get(), get()) } + viewModel { HistoryListViewModel(get(), get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 8422232bd..97fa18c4c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -14,6 +14,7 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.MangaWithHistory import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.daysDiff @@ -24,7 +25,8 @@ import java.util.concurrent.TimeUnit class HistoryListViewModel( private val repository: HistoryRepository, private val settings: AppSettings, - private val shortcutsRepository: ShortcutsRepository + private val shortcutsRepository: ShortcutsRepository, + private val trackingRepository: TrackingRepository, ) : MangaListViewModel(settings) { val onItemRemoved = SingleLiveEvent() @@ -75,7 +77,7 @@ class HistoryListViewModel( settings.historyGrouping = isGroupingEnabled } - private fun mapList( + private suspend fun mapList( list: List, grouped: Boolean, mode: ListMode @@ -93,10 +95,11 @@ class HistoryListViewModel( } prevDate = date } + val counter = trackingRepository.getNewChaptersCount(manga.id) result += when (mode) { - ListMode.LIST -> manga.toListModel() - ListMode.DETAILED_LIST -> manga.toListDetailedModel() - ListMode.GRID -> manga.toGridModel() + ListMode.LIST -> manga.toListModel(counter) + ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter) + ListMode.GRID -> manga.toGridModel(counter) } } return result diff --git a/app/src/main/java/org/koitharu/kotatsu/list/domain/CountersProvider.kt b/app/src/main/java/org/koitharu/kotatsu/list/domain/CountersProvider.kt new file mode 100644 index 000000000..e9594019c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/domain/CountersProvider.kt @@ -0,0 +1,6 @@ +package org.koitharu.kotatsu.list.domain + +fun interface CountersProvider { + + suspend fun getCounter(mangaId: Long): Int +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt new file mode 100644 index 000000000..10c92e46a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt @@ -0,0 +1,44 @@ +@file:SuppressLint("UnsafeOptInUsageError") +package org.koitharu.kotatsu.list.ui.adapter + +import android.annotation.SuppressLint +import android.view.View +import androidx.core.view.doOnNextLayout +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import org.koitharu.kotatsu.R + +fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? { + return if (counter > 0) { + val badgeDrawable = badge ?: initBadge(this) + badgeDrawable.number = counter + badgeDrawable.isVisible = true + badgeDrawable.align() + badgeDrawable + } else { + badge?.isVisible = false + badge + } +} + +fun View.clearBadge(badge: BadgeDrawable?) { + BadgeUtils.detachBadgeDrawable(badge, this) +} + +private fun initBadge(anchor: View): BadgeDrawable { + val badge = BadgeDrawable.create(anchor.context) + val resources = anchor.resources + badge.maxCharacterCount = resources.getInteger(R.integer.manga_badge_max_character_count) + badge.horizontalOffsetWithoutText = resources.getDimensionPixelOffset(R.dimen.manga_badge_offset_horizontal) + badge.verticalOffsetWithoutText = resources.getDimensionPixelOffset(R.dimen.manga_badge_offset_vertical) + anchor.doOnNextLayout { + BadgeUtils.attachBadgeDrawable(badge, it) + badge.align() + } + return badge +} + +private fun BadgeDrawable.align() { + horizontalOffsetWithText = horizontalOffsetWithoutText + intrinsicWidth / 2 + verticalOffsetWithText = verticalOffsetWithoutText + intrinsicHeight / 2 +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index 4c5ee8176..002281c70 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable import coil.util.CoilUtils +import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener @@ -24,6 +25,7 @@ fun mangaGridItemAD( ) { var imageRequest: Disposable? = null + var badge: BadgeDrawable? = null itemView.setOnClickListener { clickListener.onItemClick(item.manga, it) @@ -43,9 +45,12 @@ fun mangaGridItemAD( .allowRgb565(true) .lifecycle(lifecycleOwner) .enqueueWith(coil) + badge = itemView.bindBadge(badge, item.counter) } onViewRecycled { + itemView.clearBadge(badge) + badge = null imageRequest?.dispose() CoilUtils.clear(binding.imageViewCover) binding.imageViewCover.setImageDrawable(null) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 8927857fb..042540184 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable import coil.util.CoilUtils +import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener @@ -25,6 +26,7 @@ fun mangaListDetailedItemAD( ) { var imageRequest: Disposable? = null + var badge: BadgeDrawable? = null itemView.setOnClickListener { clickListener.onItemClick(item.manga, it) @@ -47,9 +49,12 @@ fun mangaListDetailedItemAD( .enqueueWith(coil) binding.textViewRating.textAndVisible = item.rating binding.textViewTags.text = item.tags + itemView.bindBadge(badge, item.counter) } onViewRecycled { + itemView.clearBadge(badge) + badge = null imageRequest?.dispose() CoilUtils.clear(binding.imageViewCover) binding.imageViewCover.setImageDrawable(null) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 18c99374a..0a17d4bc5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import coil.ImageLoader import coil.request.Disposable import coil.util.CoilUtils +import com.google.android.material.badge.BadgeDrawable import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener @@ -25,6 +26,7 @@ fun mangaListItemAD( ) { var imageRequest: Disposable? = null + var badge: BadgeDrawable? = null itemView.setOnClickListener { clickListener.onItemClick(item.manga, it) @@ -45,9 +47,12 @@ fun mangaListItemAD( .allowRgb565(true) .lifecycle(lifecycleOwner) .enqueueWith(coil) + itemView.bindBadge(badge, item.counter) } onViewRecycled { + itemView.clearBadge(badge) + badge = null imageRequest?.dispose() CoilUtils.clear(binding.imageViewCover) binding.imageViewCover.setImageDrawable(null) diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt index b7a367591..749e38369 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/ListModelConversionExt.kt @@ -6,44 +6,71 @@ import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.prefs.ListMode +import org.koitharu.kotatsu.list.domain.CountersProvider -fun Manga.toListModel() = MangaListModel( +fun Manga.toListModel(counter: Int) = MangaListModel( id = id, title = title, subtitle = tags.joinToString(", ") { it.title }, coverUrl = coverUrl, - manga = this + manga = this, + counter = counter, ) -fun Manga.toListDetailedModel() = MangaListDetailedModel( +fun Manga.toListDetailedModel(counter: Int) = MangaListDetailedModel( id = id, title = title, subtitle = altTitle, rating = if (rating == Manga.NO_RATING) null else String.format("%.1f", rating * 5), tags = tags.joinToString(", ") { it.title }, coverUrl = coverUrl, - manga = this + manga = this, + counter = counter, ) -fun Manga.toGridModel() = MangaGridModel( +fun Manga.toGridModel(counter: Int) = MangaGridModel( id = id, title = title, coverUrl = coverUrl, - manga = this + manga = this, + counter = counter, ) -fun List.toUi(mode: ListMode): List = when (mode) { - ListMode.LIST -> map(Manga::toListModel) - ListMode.DETAILED_LIST -> map(Manga::toListDetailedModel) - ListMode.GRID -> map(Manga::toGridModel) +suspend fun List.toUi( + mode: ListMode, + countersProvider: CountersProvider, +): List = when (mode) { + ListMode.LIST -> map { it.toListModel(countersProvider.getCounter(it.id)) } + ListMode.DETAILED_LIST -> map { it.toListDetailedModel(countersProvider.getCounter(it.id)) } + ListMode.GRID -> map { it.toGridModel(countersProvider.getCounter(it.id)) } } -fun > List.toUi(destination: C, mode: ListMode): C = - when (mode) { - ListMode.LIST -> mapTo(destination, Manga::toListModel) - ListMode.DETAILED_LIST -> mapTo(destination, Manga::toListDetailedModel) - ListMode.GRID -> mapTo(destination, Manga::toGridModel) - } +suspend fun > List.toUi( + destination: C, + mode: ListMode, + countersProvider: CountersProvider, +): C = when (mode) { + ListMode.LIST -> mapTo(destination) { it.toListModel(countersProvider.getCounter(it.id)) } + ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(countersProvider.getCounter(it.id)) } + ListMode.GRID -> mapTo(destination) { it.toGridModel(countersProvider.getCounter(it.id)) } +} + +fun List.toUi( + mode: ListMode, +): List = when (mode) { + ListMode.LIST -> map { it.toListModel(0) } + ListMode.DETAILED_LIST -> map { it.toListDetailedModel(0) } + ListMode.GRID -> map { it.toGridModel(0) } +} + +fun > List.toUi( + destination: C, + mode: ListMode, +): C = when (mode) { + ListMode.LIST -> mapTo(destination) { it.toListModel(0) } + ListMode.DETAILED_LIST -> mapTo(destination) { it.toListDetailedModel(0) } + ListMode.GRID -> mapTo(destination) { it.toGridModel(0) } +} fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState( exception = this, diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt index 1dc1e245c..3ba8b6b37 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaGridModel.kt @@ -6,5 +6,6 @@ data class MangaGridModel( val id: Long, val title: String, val coverUrl: String, - val manga: Manga + val manga: Manga, + val counter: Int, ) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt index cb9b6b2a9..73735224c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListDetailedModel.kt @@ -9,5 +9,6 @@ data class MangaListDetailedModel( val tags: String, val coverUrl: String, val rating: String?, - val manga: Manga + val manga: Manga, + val counter: Int, ) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt index 1f7d4aab1..603f6b197 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/model/MangaListModel.kt @@ -7,5 +7,6 @@ data class MangaListModel( val title: String, val subtitle: String, val coverUrl: String, - val manga: Manga + val manga: Manga, + val counter: Int, ) : ListModel \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index f89f50d35..3f721355a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -43,7 +43,13 @@ class LocalListViewModel( when { error != null -> listOf(error.toErrorState(canRetry = true)) list == null -> listOf(LoadingState) - list.isEmpty() -> listOf(EmptyState(R.drawable.ic_storage, R.string.text_local_holder_primary, R.string.text_local_holder_secondary)) + list.isEmpty() -> listOf( + EmptyState( + R.drawable.ic_storage, + R.string.text_local_holder_primary, + R.string.text_local_holder_secondary + ) + ) else -> ArrayList(list.size + 1).apply { add(headerModel) list.toUi(this, mode) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt index 8e00198de..7b5aa6fd2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/domain/TrackingRepository.kt @@ -12,8 +12,7 @@ class TrackingRepository( ) { suspend fun getNewChaptersCount(mangaId: Long): Int { - val entity = db.tracksDao.find(mangaId) ?: return 0 - return entity.newChapters + return db.tracksDao.findNewChapters(mangaId) ?: 0 } suspend fun getAllTracks(useFavourites: Boolean, useHistory: Boolean): List { diff --git a/app/src/main/res/layout/item_manga_grid.xml b/app/src/main/res/layout/item_manga_grid.xml index 866f9c975..83481a2ec 100644 --- a/app/src/main/res/layout/item_manga_grid.xml +++ b/app/src/main/res/layout/item_manga_grid.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/list_selector" + android:clipChildren="false" android:orientation="vertical"> 48dp 16dp 72dp + 4dp + 2dp 22sp diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index 5532e13df..794917074 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -2,5 +2,5 @@ @android:integer/config_shortAnimTime - + 3 \ No newline at end of file