Show reverse progress and chapters in lists #904

master
Koitharu 2 years ago
parent 607785dcd4
commit 5363719643
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -18,14 +18,14 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import javax.inject.Inject import javax.inject.Inject
class MigrateUseCase class MigrateUseCase
@Inject @Inject
constructor( constructor(
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
private val database: MangaDatabase, private val database: MangaDatabase,
private val progressUpdateUseCase: ProgressUpdateUseCase, private val progressUpdateUseCase: ProgressUpdateUseCase,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>, private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
) { ) {
suspend operator fun invoke( suspend operator fun invoke(
oldManga: Manga, oldManga: Manga,
newManga: Manga, newManga: Manga,
@ -142,7 +142,7 @@ class MigrateUseCase
scroll = history.scroll, scroll = history.scroll,
percent = history.percent, percent = history.percent,
deletedAt = 0, deletedAt = 0,
chaptersCount = chapters.size, chaptersCount = chapters.count { it.branch == currentChapter.branch },
) )
} }
val branch = oldManga.getPreferredBranch(history.toMangaHistory()) val branch = oldManga.getPreferredBranch(history.toMangaHistory())
@ -192,4 +192,4 @@ class MigrateUseCase
} else { } else {
firstOrNull { it.volume == volume && it.number == number } firstOrNull { it.volume == volume && it.number == number }
} }
} }

@ -62,7 +62,7 @@ fun alternativeAD(
} }
} }
} }
binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads) binding.progressView.setProgress(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads)
binding.chipSource.also { chip -> binding.chipSource.also { chip ->
chip.text = item.manga.source.getTitle(chip.context) chip.text = item.manga.source.getTitle(chip.context)
ImageRequest.Builder(context) ImageRequest.Builder(context)

@ -21,7 +21,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.list.domain.ReadingProgress
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.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingFooter import org.koitharu.kotatsu.list.ui.model.LoadingFooter
@ -89,11 +89,7 @@ class AlternativesViewModel @Inject constructor(
} }
} }
private suspend fun getProgress(mangaId: Long): Float { private suspend fun getProgress(mangaId: Long): ReadingProgress? {
return if (settings.isReadingIndicatorsEnabled) { return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
}
} }
} }

@ -1,12 +1,13 @@
package org.koitharu.kotatsu.alternatives.ui package org.koitharu.kotatsu.alternatives.ui
import org.koitharu.kotatsu.core.model.chaptersCount import org.koitharu.kotatsu.core.model.chaptersCount
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaAlternativeModel( data class MangaAlternativeModel(
val manga: Manga, val manga: Manga,
val progress: Float, val progress: ReadingProgress?,
private val referenceChapters: Int, private val referenceChapters: Int,
) : ListModel { ) : ListModel {

@ -37,6 +37,6 @@ fun bookmarkLargeAD(
source(item.manga.source) source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }
binding.progressView.percent = item.percent binding.progressView.setProgress(item.percent, false)
} }
} }

@ -192,8 +192,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_FEED_HEADER, true) get() = prefs.getBoolean(KEY_FEED_HEADER, true)
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) } set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
val isReadingIndicatorsEnabled: Boolean val progressIndicatorMode: ProgressIndicatorMode
get() = prefs.getBoolean(KEY_READING_INDICATORS, true) get() = prefs.getEnumValue(KEY_PROGRESS_INDICATORS, ProgressIndicatorMode.PERCENT_READ)
val isHistoryExcludeNsfw: Boolean val isHistoryExcludeNsfw: Boolean
get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false) get() = prefs.getBoolean(KEY_HISTORY_EXCLUDE_NSFW, false)
@ -619,7 +619,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last" const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
const val KEY_HISTORY_GROUPING = "history_grouping" const val KEY_HISTORY_GROUPING = "history_grouping"
const val KEY_UPDATED_GROUPING = "updated_grouping" const val KEY_UPDATED_GROUPING = "updated_grouping"
const val KEY_READING_INDICATORS = "reading_indicators" const val KEY_PROGRESS_INDICATORS = "progress_indicators"
const val KEY_REVERSE_CHAPTERS = "reverse_chapters" const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters" const val KEY_GRID_VIEW_CHAPTERS = "grid_view_chapters"
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw" const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"

@ -0,0 +1,6 @@
package org.koitharu.kotatsu.core.prefs
enum class ProgressIndicatorMode {
NONE, PERCENT_READ, PERCENT_LEFT, CHAPTERS_READ, CHAPTERS_LEFT;
}

@ -45,7 +45,7 @@ class RelatedListViewModel @Inject constructor(
override val content = combine( override val content = combine(
mangaList, mangaList,
listMode, observeListModeWithTriggers(),
listError, listError,
) { list, mode, error -> ) { list, mode, error ->
when { when {

@ -28,7 +28,6 @@ import org.koitharu.kotatsu.explore.domain.ExploreRepository
import org.koitharu.kotatsu.explore.ui.model.ExploreButtons import org.koitharu.kotatsu.explore.ui.model.ExploreButtons
import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem import org.koitharu.kotatsu.explore.ui.model.MangaSourceItem
import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem import org.koitharu.kotatsu.explore.ui.model.RecommendationsItem
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.model.EmptyHint import org.koitharu.kotatsu.list.ui.model.EmptyHint
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
@ -197,7 +196,8 @@ class ExploreViewModel @Inject constructor(
coverUrl = manga.coverUrl, coverUrl = manga.coverUrl,
manga = manga, manga = manga,
counter = 0, counter = 0,
progress = PROGRESS_NONE, progress = null,
isFavorite = false,
) )
} }

@ -115,6 +115,9 @@ abstract class FavouritesDao {
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC") @Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC")
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long> abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
@Query("SELECT COUNT(category_id) FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
abstract suspend fun findCategoriesCount(mangaId: Long): Int
/** INSERT **/ /** INSERT **/
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)

@ -115,6 +115,10 @@ class FavouritesRepository @Inject constructor(
return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory() return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory()
} }
suspend fun isFavorite(mangaId: Long): Boolean {
return db.getFavouritesDao().findCategoriesCount(mangaId) != 0
}
suspend fun getCategoriesIds(mangaIds: Collection<Long>): Set<Long> { suspend fun getCategoriesIds(mangaIds: Collection<Long>): Set<Long> {
return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet() return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet()
} }

@ -66,7 +66,7 @@ class FavouritesListViewModel @Inject constructor(
override val content = combine( override val content = combine(
observeFavorites(), observeFavorites(),
listMode, observeListModeWithTriggers(),
refreshTrigger, refreshTrigger,
) { list, mode, _ -> ) { list, mode, _ ->
when { when {

@ -88,7 +88,7 @@ abstract class HistoryDao {
abstract suspend fun insert(entity: HistoryEntity): Long abstract suspend fun insert(entity: HistoryEntity): Long
@Query( @Query(
"UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, percent = :percent, updated_at = :updatedAt, deleted_at = 0 WHERE manga_id = :mangaId", "UPDATE history SET page = :page, chapter_id = :chapterId, scroll = :scroll, percent = :percent, updated_at = :updatedAt, chapters = :chapters, deleted_at = 0 WHERE manga_id = :mangaId",
) )
abstract suspend fun update( abstract suspend fun update(
mangaId: Long, mangaId: Long,
@ -96,6 +96,7 @@ abstract class HistoryDao {
chapterId: Long, chapterId: Long,
scroll: Float, scroll: Float,
percent: Float, percent: Float,
chapters: Int,
updatedAt: Long, updatedAt: Long,
): Int ): Int
@ -116,6 +117,7 @@ abstract class HistoryDao {
chapterId = entity.chapterId, chapterId = entity.chapterId,
scroll = entity.scroll, scroll = entity.scroll,
percent = entity.percent, percent = entity.percent,
chapters = entity.chaptersCount,
updatedAt = entity.updatedAt, updatedAt = entity.updatedAt,
) )

@ -19,10 +19,12 @@ import org.koitharu.kotatsu.core.model.isLocal
import org.koitharu.kotatsu.core.model.isNsfw import org.koitharu.kotatsu.core.model.isNsfw
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
import org.koitharu.kotatsu.core.ui.util.ReversibleHandle import org.koitharu.kotatsu.core.ui.util.ReversibleHandle
import org.koitharu.kotatsu.core.util.ext.mapItems import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.history.domain.model.MangaWithHistory import org.koitharu.kotatsu.history.domain.model.MangaWithHistory
import org.koitharu.kotatsu.list.domain.ListSortOrder import org.koitharu.kotatsu.list.domain.ListSortOrder
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
@ -102,6 +104,7 @@ class HistoryRepository @Inject constructor(
assert(manga.chapters != null) assert(manga.chapters != null)
db.withTransaction { db.withTransaction {
mangaRepository.storeManga(manga) mangaRepository.storeManga(manga)
val branch = manga.chapters?.findById(chapterId)?.branch
db.getHistoryDao().upsert( db.getHistoryDao().upsert(
HistoryEntity( HistoryEntity(
mangaId = manga.id, mangaId = manga.id,
@ -111,7 +114,7 @@ class HistoryRepository @Inject constructor(
page = page, page = page,
scroll = scroll.toFloat(), // we migrate to int, but decide to not update database scroll = scroll.toFloat(), // we migrate to int, but decide to not update database
percent = percent, percent = percent,
chaptersCount = manga.chapters?.size ?: -1, chaptersCount = manga.chapters?.count { it.branch == branch } ?: 0,
deletedAt = 0L, deletedAt = 0L,
), ),
) )
@ -124,8 +127,13 @@ class HistoryRepository @Inject constructor(
return db.getHistoryDao().find(manga.id)?.recoverIfNeeded(manga)?.toMangaHistory() return db.getHistoryDao().find(manga.id)?.recoverIfNeeded(manga)?.toMangaHistory()
} }
suspend fun getProgress(mangaId: Long): Float { suspend fun getProgress(mangaId: Long, mode: ProgressIndicatorMode): ReadingProgress? {
return db.getHistoryDao().findProgress(mangaId) ?: PROGRESS_NONE val entity = db.getHistoryDao().find(mangaId) ?: return null
return ReadingProgress(
percent = entity.percent,
totalChapters = entity.chaptersCount,
mode = mode,
).takeIf { it.isValid() }
} }
suspend fun clear() { suspend fun clear() {

@ -88,7 +88,7 @@ class HistoryListViewModel @Inject constructor(
override val content = combine( override val content = combine(
observeHistory(), observeHistory(),
isGroupingEnabled, isGroupingEnabled,
listMode, observeListModeWithTriggers(),
networkState, networkState,
settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled }, settings.observeAsFlow(AppSettings.KEY_INCOGNITO_MODE) { isIncognitoModeEnabled },
) { list, grouped, mode, online, incognito -> ) { list, grouped, mode, online, incognito ->

@ -26,7 +26,6 @@ class ReadingProgressDrawable(
private val outlineColor: Int private val outlineColor: Int
private val backgroundColor: Int private val backgroundColor: Int
private val textColor: Int private val textColor: Int
private val textPattern = context.getString(R.string.percent_string_pattern)
private val textBounds = Rect() private val textBounds = Rect()
private val tempRect = Rect() private val tempRect = Rect()
private val hasBackground: Boolean private val hasBackground: Boolean
@ -36,14 +35,18 @@ class ReadingProgressDrawable(
private val desiredWidth: Int private val desiredWidth: Int
private val autoFitTextSize: Boolean private val autoFitTextSize: Boolean
var progress: Float = PROGRESS_NONE var percent: Float = PROGRESS_NONE
set(value) {
field = value
invalidateSelf()
}
var text = ""
set(value) { set(value) {
field = value field = value
text = textPattern.format((value * 100f).toInt().toString())
paint.getTextBounds(text, 0, text.length, textBounds) paint.getTextBounds(text, 0, text.length, textBounds)
invalidateSelf() invalidateSelf()
} }
private var text = ""
init { init {
val ta = context.obtainStyledAttributes(styleResId, R.styleable.ProgressDrawable) val ta = context.obtainStyledAttributes(styleResId, R.styleable.ProgressDrawable)
@ -79,7 +82,7 @@ class ReadingProgressDrawable(
} }
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
if (progress < 0f) { if (percent < 0f) {
return return
} }
val cx = bounds.exactCenterX() val cx = bounds.exactCenterX()
@ -103,12 +106,12 @@ class ReadingProgressDrawable(
cx + innerRadius, cx + innerRadius,
cy + innerRadius, cy + innerRadius,
-90f, -90f,
360f * progress, 360f * percent,
false, false,
paint, paint,
) )
if (hasText) { if (hasText) {
if (checkDrawable != null && progress >= 1f - Math.ulp(progress)) { if (checkDrawable != null && percent >= 1f - Math.ulp(percent)) {
tempRect.set(bounds) tempRect.set(bounds)
tempRect.scale(0.6) tempRect.scale(0.6)
checkDrawable.bounds = tempRect checkDrawable.bounds = tempRect

@ -11,8 +11,14 @@ import android.view.animation.AccelerateDecelerateInterpolator
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_LEFT
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_READ
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.NONE
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_LEFT
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_READ
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
import org.koitharu.kotatsu.history.data.PROGRESS_NONE import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ReadingProgress
class ReadingProgressView @JvmOverloads constructor( class ReadingProgressView @JvmOverloads constructor(
context: Context, context: Context,
@ -20,17 +26,30 @@ class ReadingProgressView @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0, @AttrRes defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener { ) : View(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
private val percentPattern = context.getString(R.string.percent_string_pattern)
private var percentAnimator: ValueAnimator? = null private var percentAnimator: ValueAnimator? = null
private val animationDuration = context.getAnimationDuration(android.R.integer.config_shortAnimTime) private val animationDuration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
@StyleRes @StyleRes
private val drawableStyle: Int private val drawableStyle: Int
var percent: Float var progress: ReadingProgress? = null
get() = peekProgressDrawable()?.progress ?: PROGRESS_NONE
set(value) { set(value) {
field = value
cancelAnimation() cancelAnimation()
getProgressDrawable().progress = value getProgressDrawable().also {
it.percent = value?.percent ?: PROGRESS_NONE
it.text = when (value?.mode) {
null,
NONE -> ""
PERCENT_READ -> percentPattern.format((value.percent * 100f).toInt().toString())
PERCENT_LEFT -> "-" + percentPattern.format((value.percentLeft * 100f).toInt().toString())
CHAPTERS_READ -> value.chapters.toString()
CHAPTERS_LEFT -> "-" + value.chaptersLeft.toString()
}
}
} }
init { init {
@ -39,7 +58,11 @@ class ReadingProgressView @JvmOverloads constructor(
ta.recycle() ta.recycle()
outlineProvider = OutlineProvider() outlineProvider = OutlineProvider()
if (isInEditMode) { if (isInEditMode) {
percent = 0.27f progress = ReadingProgress(
percent = 0.27f,
totalChapters = 20,
mode = PERCENT_READ,
)
} }
} }
@ -53,7 +76,7 @@ class ReadingProgressView @JvmOverloads constructor(
override fun onAnimationUpdate(animation: ValueAnimator) { override fun onAnimationUpdate(animation: ValueAnimator) {
val p = animation.animatedValue as Float val p = animation.animatedValue as Float
getProgressDrawable().progress = p getProgressDrawable().percent = p
} }
override fun onAnimationStart(animation: Animator) = Unit override fun onAnimationStart(animation: Animator) = Unit
@ -68,16 +91,25 @@ class ReadingProgressView @JvmOverloads constructor(
override fun onAnimationRepeat(animation: Animator) = Unit override fun onAnimationRepeat(animation: Animator) = Unit
fun setPercent(value: Float, animate: Boolean) { fun setProgress(percent: Float, animate: Boolean) {
setProgress(
value = ReadingProgress(percent, 1, PERCENT_READ),
animate = animate,
)
}
fun setProgress(value: ReadingProgress?, animate: Boolean) {
val currentDrawable = peekProgressDrawable() val currentDrawable = peekProgressDrawable()
if (!animate || currentDrawable == null || value == PROGRESS_NONE) { if (!animate || currentDrawable == null || value == null) {
percent = value progress = value
return return
} }
percentAnimator?.cancel() percentAnimator?.cancel()
val currentPercent = currentDrawable.percent.coerceAtLeast(0f)
progress = value.copy(percent = currentPercent)
percentAnimator = ValueAnimator.ofFloat( percentAnimator = ValueAnimator.ofFloat(
currentDrawable.progress.coerceAtLeast(0f), currentDrawable.percent.coerceAtLeast(0f),
value, value.percent,
).apply { ).apply {
duration = animationDuration duration = animationDuration
interpolator = AccelerateDecelerateInterpolator() interpolator = AccelerateDecelerateInterpolator()

@ -11,7 +11,6 @@ import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.data.HistoryRepository import org.koitharu.kotatsu.history.data.HistoryRepository
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel
@ -59,6 +58,7 @@ class MangaListMapper @Inject constructor(
manga = manga, manga = manga,
counter = getCounter(manga.id), counter = getCounter(manga.id),
progress = getProgress(manga.id), progress = getProgress(manga.id),
isFavorite = isFavorite(manga.id),
) )
suspend fun toDetailedListModel(manga: Manga) = MangaDetailedListModel( suspend fun toDetailedListModel(manga: Manga) = MangaDetailedListModel(
@ -69,6 +69,7 @@ class MangaListMapper @Inject constructor(
manga = manga, manga = manga,
counter = getCounter(manga.id), counter = getCounter(manga.id),
progress = getProgress(manga.id), progress = getProgress(manga.id),
isFavorite = isFavorite(manga.id),
tags = mapTags(manga.tags), tags = mapTags(manga.tags),
) )
@ -79,6 +80,7 @@ class MangaListMapper @Inject constructor(
manga = manga, manga = manga,
counter = getCounter(manga.id), counter = getCounter(manga.id),
progress = getProgress(manga.id), progress = getProgress(manga.id),
isFavorite = isFavorite(manga.id),
) )
fun mapTags(tags: Collection<MangaTag>) = tags.map { fun mapTags(tags: Collection<MangaTag>) = tags.map {
@ -97,12 +99,12 @@ class MangaListMapper @Inject constructor(
} }
} }
private suspend fun getProgress(mangaId: Long): Float { private suspend fun getProgress(mangaId: Long): ReadingProgress? {
return if (settings.isReadingIndicatorsEnabled) { return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
historyRepository.getProgress(mangaId)
} else {
PROGRESS_NONE
} }
private fun isFavorite(mangaId: Long): Boolean {
return false // TODO favouritesRepository.isFavorite(mangaId)
} }
@ColorRes @ColorRes

@ -0,0 +1,35 @@
package org.koitharu.kotatsu.list.domain
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_LEFT
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.CHAPTERS_READ
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.NONE
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_LEFT
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode.PERCENT_READ
data class ReadingProgress(
val percent: Float,
val totalChapters: Int,
val mode: ProgressIndicatorMode,
) {
val percentLeft: Float
get() = 1f - percent
val chapters: Int
get() = (totalChapters * percent).toInt()
val chaptersLeft: Int
get() = (totalChapters * percentLeft).toInt()
fun isValid() = when (mode) {
NONE -> false
PERCENT_READ,
PERCENT_LEFT -> percent in 0f..1f
CHAPTERS_READ,
CHAPTERS_LEFT -> totalChapters > 0 && percent in 0f..1f
}
fun isReversed() = mode == PERCENT_LEFT || mode == CHAPTERS_LEFT
}

@ -2,11 +2,16 @@ package org.koitharu.kotatsu.list.ui
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
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.prefs.observeAsStateFlow import org.koitharu.kotatsu.core.prefs.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
@ -55,4 +60,13 @@ abstract class MangaListViewModel(
} else { } else {
this this
} }
protected fun observeListModeWithTriggers(): Flow<ListMode> = combine(
listMode,
settings.observe().filter { key ->
key == AppSettings.KEY_PROGRESS_INDICATORS || key == AppSettings.KEY_TRACKER_ENABLED
}.onStart { emit("") }
) { mode, _ ->
mode
}
} }

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.adapter package org.koitharu.kotatsu.list.ui.adapter
import android.view.View import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader import coil.ImageLoader
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
@ -14,7 +15,7 @@ import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat import org.koitharu.kotatsu.core.util.ext.setOnContextClickListenerCompat
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.databinding.ItemMangaGridBinding import org.koitharu.kotatsu.databinding.ItemMangaGridBinding
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaGridModel import org.koitharu.kotatsu.list.ui.model.MangaGridModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
@ -41,7 +42,8 @@ fun mangaGridItemAD(
bind { payloads -> bind { payloads ->
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads) binding.progressView.setProgress(item.progress, PAYLOAD_PROGRESS_CHANGED in payloads)
binding.imageViewFavorite.isVisible = item.isFavorite
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run { binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover)) size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context) defaultPlaceholders(context)

@ -39,7 +39,10 @@ fun mangaListDetailedItemAD(
bind { payloads -> bind { payloads ->
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
binding.textViewAuthor.textAndVisible = item.manga.author binding.textViewAuthor.textAndVisible = item.manga.author
binding.progressView.setPercent(item.progress, ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads) binding.progressView.setProgress(
value = item.progress,
animate = ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED in payloads,
)
binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run { binding.imageViewCover.newImageRequest(lifecycleOwner, item.coverUrl)?.run {
size(CoverSizeResolver(binding.imageViewCover)) size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context) defaultPlaceholders(context)

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaCompactListModel( data class MangaCompactListModel(
@ -9,5 +10,6 @@ data class MangaCompactListModel(
override val coverUrl: String, override val coverUrl: String,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: Float, override val progress: ReadingProgress?,
override val isFavorite: Boolean,
) : MangaListModel() ) : MangaListModel()

@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.core.ui.widgets.ChipsView import org.koitharu.kotatsu.core.ui.widgets.ChipsView
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaDetailedListModel( data class MangaDetailedListModel(
@ -10,6 +11,7 @@ data class MangaDetailedListModel(
override val coverUrl: String, override val coverUrl: String,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: Float, override val progress: ReadingProgress?,
override val isFavorite: Boolean,
val tags: List<ChipsView.ChipModel>, val tags: List<ChipsView.ChipModel>,
) : MangaListModel() ) : MangaListModel()

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
data class MangaGridModel( data class MangaGridModel(
@ -8,5 +9,6 @@ data class MangaGridModel(
override val coverUrl: String, override val coverUrl: String,
override val manga: Manga, override val manga: Manga,
override val counter: Int, override val counter: Int,
override val progress: Float, override val progress: ReadingProgress?,
override val isFavorite: Boolean,
) : MangaListModel() ) : MangaListModel()

@ -1,6 +1,8 @@
package org.koitharu.kotatsu.list.ui.model package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.domain.ReadingProgress
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_ANYTHING_CHANGED
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback.Companion.PAYLOAD_PROGRESS_CHANGED
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
@ -11,7 +13,8 @@ sealed class MangaListModel : ListModel {
abstract val title: String abstract val title: String
abstract val coverUrl: String abstract val coverUrl: String
abstract val counter: Int abstract val counter: Int
abstract val progress: Float abstract val isFavorite: Boolean
abstract val progress: ReadingProgress?
val source: MangaSource val source: MangaSource
get() = manga.source get() = manga.source
@ -20,12 +23,12 @@ sealed class MangaListModel : ListModel {
return other is MangaListModel && other.javaClass == javaClass && id == other.id return other is MangaListModel && other.javaClass == javaClass && id == other.id
} }
override fun getChangePayload(previousState: ListModel): Any? { override fun getChangePayload(previousState: ListModel): Any? = when {
return when { previousState !is MangaListModel || previousState.manga != manga -> null
previousState !is MangaListModel -> super.getChangePayload(previousState)
progress != previousState.progress -> ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
counter != previousState.counter -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED previousState.isFavorite != isFavorite || previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED
else -> null else -> null
} }
}
} }

@ -79,7 +79,7 @@ open class RemoteListViewModel @Inject constructor(
override val content = combine( override val content = combine(
mangaList.map { it?.skipNsfwIfNeeded() }, mangaList.map { it?.skipNsfwIfNeeded() },
listMode, observeListModeWithTriggers(),
listError, listError,
hasNextPage, hasNextPage,
) { list, mode, error, hasNext -> ) { list, mode, error, hasNext ->

@ -50,7 +50,7 @@ class SearchViewModel @Inject constructor(
override val content = combine( override val content = combine(
mangaList.map { it?.skipNsfwIfNeeded() }, mangaList.map { it?.skipNsfwIfNeeded() },
listMode, observeListModeWithTriggers(),
listError, listError,
hasNextPage, hasNextPage,
) { list, mode, error, hasNext -> ) { list, mode, error, hasNext ->

@ -14,6 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
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.ProgressIndicatorMode
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.LocaleComparator import org.koitharu.kotatsu.core.util.LocaleComparator
@ -44,6 +45,10 @@ class AppearanceSettingsFragment :
entryValues = ListMode.entries.names() entryValues = ListMode.entries.names()
setDefaultValueCompat(ListMode.GRID.name) setDefaultValueCompat(ListMode.GRID.name)
} }
findPreference<ListPreference>(AppSettings.KEY_PROGRESS_INDICATORS)?.run {
entryValues = ProgressIndicatorMode.entries.names()
setDefaultValueCompat(ProgressIndicatorMode.PERCENT_READ.name)
}
findPreference<ActivityListPreference>(AppSettings.KEY_APP_LOCALE)?.run { findPreference<ActivityListPreference>(AppSettings.KEY_APP_LOCALE)?.run {
initLocalePicker(this) initLocalePicker(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

@ -36,7 +36,7 @@ class SuggestionsViewModel @Inject constructor(
override val content = combine( override val content = combine(
repository.observeAll(), repository.observeAll(),
listMode, observeListModeWithTriggers(),
) { list, mode -> ) { list, mode ->
when { when {
list.isEmpty() -> listOf( list.isEmpty() -> listOf(

@ -39,7 +39,7 @@ class UpdatesViewModel @Inject constructor(
override val content = combine( override val content = combine(
repository.observeUpdatedManga(), repository.observeUpdatedManga(),
settings.observeAsFlow(AppSettings.KEY_UPDATED_GROUPING) { isUpdatedGroupingEnabled }, settings.observeAsFlow(AppSettings.KEY_UPDATED_GROUPING) { isUpdatedGroupingEnabled },
listMode, observeListModeWithTriggers(),
) { mangaList, grouping, mode -> ) { mangaList, grouping, mode ->
when { when {
mangaList.isEmpty() -> listOf( mangaList.isEmpty() -> listOf(

@ -5,11 +5,11 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/custom_selectable_item_background"
android:layout_margin="2dp" android:layout_margin="2dp"
android:padding="6dp" android:background="@drawable/custom_selectable_item_background"
android:clipChildren="false" android:clipChildren="false"
android:orientation="vertical" android:orientation="vertical"
android:padding="6dp"
tools:layout_width="140dp"> tools:layout_width="140dp">
<FrameLayout <FrameLayout
@ -34,6 +34,17 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="@dimen/card_indicator_offset" /> android:layout_margin="@dimen/card_indicator_offset" />
<ImageView
android:id="@+id/imageView_favorite"
android:layout_width="@dimen/card_indicator_size"
android:layout_height="@dimen/card_indicator_size"
android:layout_gravity="bottom|start"
android:layout_margin="@dimen/card_indicator_offset"
android:contentDescription="@string/favourites"
android:scaleType="centerInside"
app:srcCompat="@drawable/ic_heart"
app:tint="?colorSurfaceBright" />
</FrameLayout> </FrameLayout>
<TextView <TextView

@ -101,4 +101,11 @@
<item>@string/pages</item> <item>@string/pages</item>
<item>@string/webtoon</item> <item>@string/webtoon</item>
</string-array> </string-array>
<string-array name="progress_indicators" translatable="false">
<item>@string/disabled</item>
<item>@string/percent_read</item>
<item>@string/percent_left</item>
<item>@string/chapters_read</item>
<item>@string/chapters_left</item>
</string-array>
</resources> </resources>

@ -664,4 +664,8 @@
<string name="sources_unpinned">Sources unpinned</string> <string name="sources_unpinned">Sources unpinned</string>
<string name="sources_pinned">Sources pinned</string> <string name="sources_pinned">Sources pinned</string>
<string name="recent_sources">Recent sources</string> <string name="recent_sources">Recent sources</string>
<string name="percent_read">Percent read</string>
<string name="percent_left">Percent left</string>
<string name="chapters_read">Chapters read</string>
<string name="chapters_left">Chapters left</string>
</resources> </resources>

@ -38,11 +38,11 @@
android:valueTo="150" android:valueTo="150"
app:defaultValue="100" /> app:defaultValue="100" />
<SwitchPreferenceCompat <ListPreference
android:defaultValue="true" android:entries="@array/progress_indicators"
android:key="reading_indicators" android:key="progress_indicators"
android:summary="@string/show_reading_indicators_summary" android:title="@string/show_reading_indicators"
android:title="@string/show_reading_indicators" /> app:useSimpleSummaryProvider="true" />
</PreferenceCategory> </PreferenceCategory>

Loading…
Cancel
Save