Merge branch 'feature/counters' into devel

pull/100/head
Koitharu 4 years ago
commit cfdc3a15c5
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -13,6 +13,9 @@ abstract class TracksDao {
@Query("SELECT * FROM tracks WHERE manga_id = :mangaId") @Query("SELECT * FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun find(mangaId: Long): TrackEntity? 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") @Query("DELETE FROM tracks")
abstract suspend fun clear() abstract suspend fun clear()

@ -13,7 +13,7 @@ val favouritesModule
single { FavouritesRepository(get()) } single { FavouritesRepository(get()) }
viewModel { categoryId -> viewModel { categoryId ->
FavouritesListViewModel(categoryId.get(), get(), get()) FavouritesListViewModel(categoryId.get(), get(), get(), get())
} }
viewModel { FavouritesCategoriesViewModel(get()) } viewModel { FavouritesCategoriesViewModel(get()) }
viewModel { manga -> 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.model.SortOrder
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository 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.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
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.list.ui.model.toUi import org.koitharu.kotatsu.list.ui.model.toUi
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
class FavouritesListViewModel( class FavouritesListViewModel(
private val categoryId: Long, private val categoryId: Long,
private val repository: FavouritesRepository, private val repository: FavouritesRepository,
settings: AppSettings private val trackingRepository: TrackingRepository,
) : MangaListViewModel(settings) { settings: AppSettings,
) : MangaListViewModel(settings), CountersProvider {
override val content = combine( override val content = combine(
if (categoryId == 0L) { if (categoryId == 0L) {
@ -42,7 +45,7 @@ class FavouritesListViewModel(
} }
) )
) )
else -> list.toUi(mode) else -> list.toUi(mode, this)
} }
}.catch { }.catch {
emit(listOf(it.toErrorState(canRetry = false))) 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 { get() = module {
single { HistoryRepository(get(), get(), get()) } 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.history.domain.MangaWithHistory
import org.koitharu.kotatsu.list.ui.MangaListViewModel import org.koitharu.kotatsu.list.ui.MangaListViewModel
import org.koitharu.kotatsu.list.ui.model.* 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.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
@ -24,7 +25,8 @@ import java.util.concurrent.TimeUnit
class HistoryListViewModel( class HistoryListViewModel(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val shortcutsRepository: ShortcutsRepository private val shortcutsRepository: ShortcutsRepository,
private val trackingRepository: TrackingRepository,
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
val onItemRemoved = SingleLiveEvent<Manga>() val onItemRemoved = SingleLiveEvent<Manga>()
@ -75,7 +77,7 @@ class HistoryListViewModel(
settings.historyGrouping = isGroupingEnabled settings.historyGrouping = isGroupingEnabled
} }
private fun mapList( private suspend fun mapList(
list: List<MangaWithHistory>, list: List<MangaWithHistory>,
grouped: Boolean, grouped: Boolean,
mode: ListMode mode: ListMode
@ -93,10 +95,11 @@ class HistoryListViewModel(
} }
prevDate = date prevDate = date
} }
val counter = trackingRepository.getNewChaptersCount(manga.id)
result += when (mode) { result += when (mode) {
ListMode.LIST -> manga.toListModel() ListMode.LIST -> manga.toListModel(counter)
ListMode.DETAILED_LIST -> manga.toListDetailedModel() ListMode.DETAILED_LIST -> manga.toListDetailedModel(counter)
ListMode.GRID -> manga.toGridModel() ListMode.GRID -> manga.toGridModel(counter)
} }
} }
return result 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.ImageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -24,6 +25,7 @@ fun mangaGridItemAD(
) { ) {
var imageRequest: Disposable? = null var imageRequest: Disposable? = null
var badge: BadgeDrawable? = null
itemView.setOnClickListener { itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it) clickListener.onItemClick(item.manga, it)
@ -43,9 +45,12 @@ fun mangaGridItemAD(
.allowRgb565(true) .allowRgb565(true)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)
badge = itemView.bindBadge(badge, item.counter)
} }
onViewRecycled { onViewRecycled {
itemView.clearBadge(badge)
badge = null
imageRequest?.dispose() imageRequest?.dispose()
CoilUtils.clear(binding.imageViewCover) CoilUtils.clear(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null) binding.imageViewCover.setImageDrawable(null)

@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -25,6 +26,7 @@ fun mangaListDetailedItemAD(
) { ) {
var imageRequest: Disposable? = null var imageRequest: Disposable? = null
var badge: BadgeDrawable? = null
itemView.setOnClickListener { itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it) clickListener.onItemClick(item.manga, it)
@ -47,9 +49,12 @@ fun mangaListDetailedItemAD(
.enqueueWith(coil) .enqueueWith(coil)
binding.textViewRating.textAndVisible = item.rating binding.textViewRating.textAndVisible = item.rating
binding.textViewTags.text = item.tags binding.textViewTags.text = item.tags
itemView.bindBadge(badge, item.counter)
} }
onViewRecycled { onViewRecycled {
itemView.clearBadge(badge)
badge = null
imageRequest?.dispose() imageRequest?.dispose()
CoilUtils.clear(binding.imageViewCover) CoilUtils.clear(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null) binding.imageViewCover.setImageDrawable(null)

@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import coil.request.Disposable import coil.request.Disposable
import coil.util.CoilUtils import coil.util.CoilUtils
import com.google.android.material.badge.BadgeDrawable
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -25,6 +26,7 @@ fun mangaListItemAD(
) { ) {
var imageRequest: Disposable? = null var imageRequest: Disposable? = null
var badge: BadgeDrawable? = null
itemView.setOnClickListener { itemView.setOnClickListener {
clickListener.onItemClick(item.manga, it) clickListener.onItemClick(item.manga, it)
@ -45,9 +47,12 @@ fun mangaListItemAD(
.allowRgb565(true) .allowRgb565(true)
.lifecycle(lifecycleOwner) .lifecycle(lifecycleOwner)
.enqueueWith(coil) .enqueueWith(coil)
itemView.bindBadge(badge, item.counter)
} }
onViewRecycled { onViewRecycled {
itemView.clearBadge(badge)
badge = null
imageRequest?.dispose() imageRequest?.dispose()
CoilUtils.clear(binding.imageViewCover) CoilUtils.clear(binding.imageViewCover)
binding.imageViewCover.setImageDrawable(null) 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.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.prefs.ListMode 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, id = id,
title = title, title = title,
subtitle = tags.joinToString(", ") { it.title }, subtitle = tags.joinToString(", ") { it.title },
coverUrl = coverUrl, coverUrl = coverUrl,
manga = this manga = this,
counter = counter,
) )
fun Manga.toListDetailedModel() = MangaListDetailedModel( fun Manga.toListDetailedModel(counter: Int) = MangaListDetailedModel(
id = id, id = id,
title = title, title = title,
subtitle = altTitle, subtitle = altTitle,
rating = if (rating == Manga.NO_RATING) null else String.format("%.1f", rating * 5), rating = if (rating == Manga.NO_RATING) null else String.format("%.1f", rating * 5),
tags = tags.joinToString(", ") { it.title }, tags = tags.joinToString(", ") { it.title },
coverUrl = coverUrl, coverUrl = coverUrl,
manga = this manga = this,
counter = counter,
) )
fun Manga.toGridModel() = MangaGridModel( fun Manga.toGridModel(counter: Int) = MangaGridModel(
id = id, id = id,
title = title, title = title,
coverUrl = coverUrl, coverUrl = coverUrl,
manga = this manga = this,
counter = counter,
) )
fun List<Manga>.toUi(mode: ListMode): List<ListModel> = when (mode) { suspend fun List<Manga>.toUi(
ListMode.LIST -> map(Manga::toListModel) mode: ListMode,
ListMode.DETAILED_LIST -> map(Manga::toListDetailedModel) countersProvider: CountersProvider,
ListMode.GRID -> map(Manga::toGridModel) ): 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 = suspend fun <C : MutableCollection<ListModel>> List<Manga>.toUi(
when (mode) { destination: C,
ListMode.LIST -> mapTo(destination, Manga::toListModel) mode: ListMode,
ListMode.DETAILED_LIST -> mapTo(destination, Manga::toListDetailedModel) countersProvider: CountersProvider,
ListMode.GRID -> mapTo(destination, Manga::toGridModel) ): 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( fun Throwable.toErrorState(canRetry: Boolean = true) = ErrorState(
exception = this, exception = this,

@ -6,5 +6,6 @@ data class MangaGridModel(
val id: Long, val id: Long,
val title: String, val title: String,
val coverUrl: String, val coverUrl: String,
val manga: Manga val manga: Manga,
val counter: Int,
) : ListModel ) : ListModel

@ -9,5 +9,6 @@ data class MangaListDetailedModel(
val tags: String, val tags: String,
val coverUrl: String, val coverUrl: String,
val rating: String?, val rating: String?,
val manga: Manga val manga: Manga,
val counter: Int,
) : ListModel ) : ListModel

@ -7,5 +7,6 @@ data class MangaListModel(
val title: String, val title: String,
val subtitle: String, val subtitle: String,
val coverUrl: String, val coverUrl: String,
val manga: Manga val manga: Manga,
val counter: Int,
) : ListModel ) : ListModel

@ -43,7 +43,13 @@ class LocalListViewModel(
when { when {
error != null -> listOf(error.toErrorState(canRetry = true)) error != null -> listOf(error.toErrorState(canRetry = true))
list == null -> listOf(LoadingState) 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 { else -> ArrayList<ListModel>(list.size + 1).apply {
add(headerModel) add(headerModel)
list.toUi(this, mode) list.toUi(this, mode)

@ -12,8 +12,7 @@ class TrackingRepository(
) { ) {
suspend fun getNewChaptersCount(mangaId: Long): Int { suspend fun getNewChaptersCount(mangaId: Long): Int {
val entity = db.tracksDao.find(mangaId) ?: return 0 return db.tracksDao.findNewChapters(mangaId) ?: 0
return entity.newChapters
} }
suspend fun getAllTracks(useFavourites: Boolean, useHistory: Boolean): List<MangaTracking> { suspend fun getAllTracks(useFavourites: Boolean, useHistory: Boolean): List<MangaTracking> {

@ -6,6 +6,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/list_selector" android:background="@drawable/list_selector"
android:clipChildren="false"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView

@ -21,6 +21,8 @@
<dimen name="list_footer_height_outer">48dp</dimen> <dimen name="list_footer_height_outer">48dp</dimen>
<dimen name="screen_padding">16dp</dimen> <dimen name="screen_padding">16dp</dimen>
<dimen name="feed_dividers_offset">72dp</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--> <!--Text dimens-->
<dimen name="text_size_h1">22sp</dimen> <dimen name="text_size_h1">22sp</dimen>

@ -2,5 +2,5 @@
<resources> <resources>
<integer name="search_animation_duration">@android:integer/config_shortAnimTime</integer> <integer name="search_animation_duration">@android:integer/config_shortAnimTime</integer>
<integer name="manga_badge_max_character_count">3</integer>
</resources> </resources>
Loading…
Cancel
Save