Merge branch 'feature/counters' into devel
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,5 @@ val historyModule
|
||||
get() = module {
|
||||
|
||||
single { HistoryRepository(get(), get(), get()) }
|
||||
viewModel { HistoryListViewModel(get(), get(), get()) }
|
||||
viewModel { HistoryListViewModel(get(), get(), get(), get()) }
|
||||
}
|
||||
@@ -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<Manga>()
|
||||
@@ -75,7 +77,7 @@ class HistoryListViewModel(
|
||||
settings.historyGrouping = isGroupingEnabled
|
||||
}
|
||||
|
||||
private fun mapList(
|
||||
private suspend fun mapList(
|
||||
list: List<MangaWithHistory>,
|
||||
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
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.koitharu.kotatsu.list.domain
|
||||
|
||||
fun interface CountersProvider {
|
||||
|
||||
suspend fun getCounter(mangaId: Long): Int
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<Manga>.toUi(mode: ListMode): List<ListModel> = when (mode) {
|
||||
ListMode.LIST -> map(Manga::toListModel)
|
||||
ListMode.DETAILED_LIST -> map(Manga::toListDetailedModel)
|
||||
ListMode.GRID -> map(Manga::toGridModel)
|
||||
suspend fun List<Manga>.toUi(
|
||||
mode: ListMode,
|
||||
countersProvider: CountersProvider,
|
||||
): List<ListModel> = 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 <C : MutableCollection<ListModel>> List<Manga>.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 <C : MutableCollection<ListModel>> List<Manga>.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<Manga>.toUi(
|
||||
mode: ListMode,
|
||||
): List<ListModel> = when (mode) {
|
||||
ListMode.LIST -> map { it.toListModel(0) }
|
||||
ListMode.DETAILED_LIST -> map { it.toListDetailedModel(0) }
|
||||
ListMode.GRID -> map { it.toGridModel(0) }
|
||||
}
|
||||
|
||||
fun <C : MutableCollection<ListModel>> List<Manga>.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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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<ListModel>(list.size + 1).apply {
|
||||
add(headerModel)
|
||||
list.toUi(this, mode)
|
||||
|
||||
@@ -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<MangaTracking> {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="@drawable/list_selector"
|
||||
android:clipChildren="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
<dimen name="list_footer_height_outer">48dp</dimen>
|
||||
<dimen name="screen_padding">16dp</dimen>
|
||||
<dimen name="feed_dividers_offset">72dp</dimen>
|
||||
<dimen name="manga_badge_offset_horizontal">4dp</dimen>
|
||||
<dimen name="manga_badge_offset_vertical">2dp</dimen>
|
||||
|
||||
<!--Text dimens-->
|
||||
<dimen name="text_size_h1">22sp</dimen>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<resources>
|
||||
|
||||
<integer name="search_animation_duration">@android:integer/config_shortAnimTime</integer>
|
||||
|
||||
<integer name="manga_badge_max_character_count">3</integer>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user