Show reverse progress and chapters in lists #904

This commit is contained in:
Koitharu
2024-07-22 18:31:04 +03:00
parent 607785dcd4
commit 5363719643
35 changed files with 373 additions and 226 deletions

View File

@@ -18,178 +18,178 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import javax.inject.Inject
class MigrateUseCase
@Inject
constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository,
private val database: MangaDatabase,
private val progressUpdateUseCase: ProgressUpdateUseCase,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
@Inject
constructor(
private val mangaRepositoryFactory: MangaRepository.Factory,
private val mangaDataRepository: MangaDataRepository,
private val database: MangaDatabase,
private val progressUpdateUseCase: ProgressUpdateUseCase,
private val scrobblers: Set<@JvmSuppressWildcards Scrobbler>,
) {
suspend operator fun invoke(
oldManga: Manga,
newManga: Manga,
) {
suspend operator fun invoke(
oldManga: Manga,
newManga: Manga,
) {
val oldDetails =
if (oldManga.chapters.isNullOrEmpty()) {
runCatchingCancellable {
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
}.getOrDefault(oldManga)
} else {
oldManga
}
val newDetails =
if (newManga.chapters.isNullOrEmpty()) {
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} else {
newManga
}
mangaDataRepository.storeManga(newDetails)
database.withTransaction {
// replace favorites
val favoritesDao = database.getFavouritesDao()
val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
if (oldFavourites.isNotEmpty()) {
favoritesDao.delete(oldManga.id)
for (f in oldFavourites) {
val e =
f.copy(
mangaId = newManga.id,
)
favoritesDao.upsert(e)
}
}
// replace history
val historyDao = database.getHistoryDao()
val oldHistory = historyDao.find(oldDetails.id)
val newHistory =
if (oldHistory != null) {
val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
historyDao.delete(oldDetails.id)
historyDao.upsert(newHistory)
newHistory
} else {
null
}
// track
val tracksDao = database.getTracksDao()
val oldTrack = tracksDao.find(oldDetails.id)
if (oldTrack != null) {
val lastChapter = newDetails.chapters?.lastOrNull()
val newTrack =
TrackEntity(
mangaId = newDetails.id,
lastChapterId = lastChapter?.id ?: 0L,
newChapters = 0,
lastCheckTime = System.currentTimeMillis(),
lastChapterDate = lastChapter?.uploadDate ?: 0L,
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
lastError = null,
val oldDetails =
if (oldManga.chapters.isNullOrEmpty()) {
runCatchingCancellable {
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
}.getOrDefault(oldManga)
} else {
oldManga
}
val newDetails =
if (newManga.chapters.isNullOrEmpty()) {
mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} else {
newManga
}
mangaDataRepository.storeManga(newDetails)
database.withTransaction {
// replace favorites
val favoritesDao = database.getFavouritesDao()
val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
if (oldFavourites.isNotEmpty()) {
favoritesDao.delete(oldManga.id)
for (f in oldFavourites) {
val e =
f.copy(
mangaId = newManga.id,
)
tracksDao.delete(oldDetails.id)
tracksDao.upsert(newTrack)
favoritesDao.upsert(e)
}
// scrobbling
for (scrobbler in scrobblers) {
if (!scrobbler.isEnabled) {
continue
}
val prevInfo = scrobbler.getScrobblingInfoOrNull(oldDetails.id) ?: continue
scrobbler.unregisterScrobbling(oldDetails.id)
scrobbler.linkManga(newDetails.id, prevInfo.targetId)
scrobbler.updateScrobblingInfo(
}
// replace history
val historyDao = database.getHistoryDao()
val oldHistory = historyDao.find(oldDetails.id)
val newHistory =
if (oldHistory != null) {
val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
historyDao.delete(oldDetails.id)
historyDao.upsert(newHistory)
newHistory
} else {
null
}
// track
val tracksDao = database.getTracksDao()
val oldTrack = tracksDao.find(oldDetails.id)
if (oldTrack != null) {
val lastChapter = newDetails.chapters?.lastOrNull()
val newTrack =
TrackEntity(
mangaId = newDetails.id,
rating = prevInfo.rating,
status =
prevInfo.status ?: when {
newHistory == null -> ScrobblingStatus.PLANNED
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING
},
comment = prevInfo.comment,
lastChapterId = lastChapter?.id ?: 0L,
newChapters = 0,
lastCheckTime = System.currentTimeMillis(),
lastChapterDate = lastChapter?.uploadDate ?: 0L,
lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
lastError = null,
)
if (newHistory != null) {
scrobbler.scrobble(
manga = newDetails,
chapterId = newHistory.chapterId,
)
}
}
tracksDao.delete(oldDetails.id)
tracksDao.upsert(newTrack)
}
progressUpdateUseCase(newManga)
}
private fun makeNewHistory(
oldManga: Manga,
newManga: Manga,
history: HistoryEntity,
): HistoryEntity {
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
val branch = newManga.getPreferredBranch(null)
val chapters = checkNotNull(newManga.getChapters(branch))
val currentChapter =
if (history.percent in 0f..1f) {
chapters[(chapters.lastIndex * history.percent).toInt()]
} else {
chapters.first()
}
return HistoryEntity(
mangaId = newManga.id,
createdAt = history.createdAt,
updatedAt = System.currentTimeMillis(),
chapterId = currentChapter.id,
page = history.page,
scroll = history.scroll,
percent = history.percent,
deletedAt = 0,
chaptersCount = chapters.size,
// scrobbling
for (scrobbler in scrobblers) {
if (!scrobbler.isEnabled) {
continue
}
val prevInfo = scrobbler.getScrobblingInfoOrNull(oldDetails.id) ?: continue
scrobbler.unregisterScrobbling(oldDetails.id)
scrobbler.linkManga(newDetails.id, prevInfo.targetId)
scrobbler.updateScrobblingInfo(
mangaId = newDetails.id,
rating = prevInfo.rating,
status =
prevInfo.status ?: when {
newHistory == null -> ScrobblingStatus.PLANNED
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
else -> ScrobblingStatus.READING
},
comment = prevInfo.comment,
)
}
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
val oldChapters = checkNotNull(oldManga.getChapters(branch))
var index = oldChapters.indexOfFirst { it.id == history.chapterId }
if (index < 0) {
index =
if (history.percent in 0f..1f) {
(oldChapters.lastIndex * history.percent).toInt()
} else {
0
}
}
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
val newBranch =
if (newChapters.containsKey(branch)) {
branch
} else {
newManga.getPreferredBranch(null)
if (newHistory != null) {
scrobbler.scrobble(
manga = newDetails,
chapterId = newHistory.chapterId,
)
}
val newChapterId =
checkNotNull(newChapters[newBranch])
.let {
val oldChapter = oldChapters[index]
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
}.id
}
}
progressUpdateUseCase(newManga)
}
private fun makeNewHistory(
oldManga: Manga,
newManga: Manga,
history: HistoryEntity,
): HistoryEntity {
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source
val branch = newManga.getPreferredBranch(null)
val chapters = checkNotNull(newManga.getChapters(branch))
val currentChapter =
if (history.percent in 0f..1f) {
chapters[(chapters.lastIndex * history.percent).toInt()]
} else {
chapters.first()
}
return HistoryEntity(
mangaId = newManga.id,
createdAt = history.createdAt,
updatedAt = System.currentTimeMillis(),
chapterId = newChapterId,
chapterId = currentChapter.id,
page = history.page,
scroll = history.scroll,
percent = PROGRESS_NONE,
percent = history.percent,
deletedAt = 0,
chaptersCount = checkNotNull(newChapters[newBranch]).size,
chaptersCount = chapters.count { it.branch == currentChapter.branch },
)
}
private fun List<MangaChapter>.findByNumber(
volume: Int,
number: Float,
): MangaChapter? =
if (number <= 0f) {
null
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
val oldChapters = checkNotNull(oldManga.getChapters(branch))
var index = oldChapters.indexOfFirst { it.id == history.chapterId }
if (index < 0) {
index =
if (history.percent in 0f..1f) {
(oldChapters.lastIndex * history.percent).toInt()
} else {
0
}
}
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
val newBranch =
if (newChapters.containsKey(branch)) {
branch
} else {
firstOrNull { it.volume == volume && it.number == number }
newManga.getPreferredBranch(null)
}
val newChapterId =
checkNotNull(newChapters[newBranch])
.let {
val oldChapter = oldChapters[index]
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
}.id
return HistoryEntity(
mangaId = newManga.id,
createdAt = history.createdAt,
updatedAt = System.currentTimeMillis(),
chapterId = newChapterId,
page = history.page,
scroll = history.scroll,
percent = PROGRESS_NONE,
deletedAt = 0,
chaptersCount = checkNotNull(newChapters[newBranch]).size,
)
}
private fun List<MangaChapter>.findByNumber(
volume: Int,
number: Float,
): MangaChapter? =
if (number <= 0f) {
null
} else {
firstOrNull { it.volume == volume && it.number == number }
}
}

View File

@@ -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 ->
chip.text = item.manga.source.getTitle(chip.context)
ImageRequest.Builder(context)

View File

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

View File

@@ -1,12 +1,13 @@
package org.koitharu.kotatsu.alternatives.ui
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.parsers.model.Manga
data class MangaAlternativeModel(
val manga: Manga,
val progress: Float,
val progress: ReadingProgress?,
private val referenceChapters: Int,
) : ListModel {

View File

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

View File

@@ -192,8 +192,8 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
get() = prefs.getBoolean(KEY_FEED_HEADER, true)
set(value) = prefs.edit { putBoolean(KEY_FEED_HEADER, value) }
val isReadingIndicatorsEnabled: Boolean
get() = prefs.getBoolean(KEY_READING_INDICATORS, true)
val progressIndicatorMode: ProgressIndicatorMode
get() = prefs.getEnumValue(KEY_PROGRESS_INDICATORS, ProgressIndicatorMode.PERCENT_READ)
val isHistoryExcludeNsfw: Boolean
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_HISTORY_GROUPING = "history_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_GRID_VIEW_CHAPTERS = "grid_view_chapters"
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"

View File

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

View File

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

View File

@@ -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.MangaSourceItem
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.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -197,7 +196,8 @@ class ExploreViewModel @Inject constructor(
coverUrl = manga.coverUrl,
manga = manga,
counter = 0,
progress = PROGRESS_NONE,
progress = null,
isFavorite = false,
)
}

View File

@@ -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")
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(onConflict = OnConflictStrategy.REPLACE)

View File

@@ -115,6 +115,10 @@ class FavouritesRepository @Inject constructor(
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> {
return db.getFavouritesDao().findCategoriesIds(mangaIds).toSet()
}

View File

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

View File

@@ -88,7 +88,7 @@ abstract class HistoryDao {
abstract suspend fun insert(entity: HistoryEntity): Long
@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(
mangaId: Long,
@@ -96,6 +96,7 @@ abstract class HistoryDao {
chapterId: Long,
scroll: Float,
percent: Float,
chapters: Int,
updatedAt: Long,
): Int
@@ -116,6 +117,7 @@ abstract class HistoryDao {
chapterId = entity.chapterId,
scroll = entity.scroll,
percent = entity.percent,
chapters = entity.chaptersCount,
updatedAt = entity.updatedAt,
)

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,14 @@ import android.view.animation.AccelerateDecelerateInterpolator
import androidx.annotation.AttrRes
import androidx.annotation.StyleRes
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.history.data.PROGRESS_NONE
import org.koitharu.kotatsu.list.domain.ReadingProgress
class ReadingProgressView @JvmOverloads constructor(
context: Context,
@@ -20,17 +26,30 @@ class ReadingProgressView @JvmOverloads constructor(
@AttrRes defStyleAttr: Int = 0,
) : View(context, attrs, defStyleAttr), ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener {
private val percentPattern = context.getString(R.string.percent_string_pattern)
private var percentAnimator: ValueAnimator? = null
private val animationDuration = context.getAnimationDuration(android.R.integer.config_shortAnimTime)
@StyleRes
private val drawableStyle: Int
var percent: Float
get() = peekProgressDrawable()?.progress ?: PROGRESS_NONE
var progress: ReadingProgress? = null
set(value) {
field = value
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 {
@@ -39,7 +58,11 @@ class ReadingProgressView @JvmOverloads constructor(
ta.recycle()
outlineProvider = OutlineProvider()
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) {
val p = animation.animatedValue as Float
getProgressDrawable().progress = p
getProgressDrawable().percent = p
}
override fun onAnimationStart(animation: Animator) = Unit
@@ -68,16 +91,25 @@ class ReadingProgressView @JvmOverloads constructor(
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()
if (!animate || currentDrawable == null || value == PROGRESS_NONE) {
percent = value
if (!animate || currentDrawable == null || value == null) {
progress = value
return
}
percentAnimator?.cancel()
val currentPercent = currentDrawable.percent.coerceAtLeast(0f)
progress = value.copy(percent = currentPercent)
percentAnimator = ValueAnimator.ofFloat(
currentDrawable.progress.coerceAtLeast(0f),
value,
currentDrawable.percent.coerceAtLeast(0f),
value.percent,
).apply {
duration = animationDuration
interpolator = AccelerateDecelerateInterpolator()

View File

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

View File

@@ -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
}

View File

@@ -2,11 +2,16 @@ package org.koitharu.kotatsu.list.ui
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
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.plus
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.observeAsStateFlow
import org.koitharu.kotatsu.core.ui.BaseViewModel
@@ -55,4 +60,13 @@ abstract class MangaListViewModel(
} else {
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
}
}

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.adapter
import android.view.View
import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
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.source
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.MangaGridModel
import org.koitharu.kotatsu.list.ui.size.ItemSizeResolver
@@ -41,7 +42,8 @@ fun mangaGridItemAD(
bind { payloads ->
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 {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)

View File

@@ -39,7 +39,10 @@ fun mangaListDetailedItemAD(
bind { payloads ->
binding.textViewTitle.text = item.title
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 {
size(CoverSizeResolver(binding.imageViewCover))
defaultPlaceholders(context)

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
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.MangaSource
@@ -11,7 +13,8 @@ sealed class MangaListModel : ListModel {
abstract val title: String
abstract val coverUrl: String
abstract val counter: Int
abstract val progress: Float
abstract val isFavorite: Boolean
abstract val progress: ReadingProgress?
val source: MangaSource
get() = manga.source
@@ -20,12 +23,12 @@ sealed class MangaListModel : ListModel {
return other is MangaListModel && other.javaClass == javaClass && id == other.id
}
override fun getChangePayload(previousState: ListModel): Any? {
return when {
previousState !is MangaListModel -> super.getChangePayload(previousState)
progress != previousState.progress -> ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED
counter != previousState.counter -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
else -> null
}
override fun getChangePayload(previousState: ListModel): Any? = when {
previousState !is MangaListModel || previousState.manga != manga -> null
previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
previousState.isFavorite != isFavorite || previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED
else -> null
}
}

View File

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

View File

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

View File

@@ -14,6 +14,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.AppSettings
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.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.util.LocaleComparator
@@ -44,6 +45,10 @@ class AppearanceSettingsFragment :
entryValues = ListMode.entries.names()
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 {
initLocalePicker(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

View File

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

View File

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

View File

@@ -5,11 +5,11 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/custom_selectable_item_background"
android:layout_margin="2dp"
android:padding="6dp"
android:background="@drawable/custom_selectable_item_background"
android:clipChildren="false"
android:orientation="vertical"
android:padding="6dp"
tools:layout_width="140dp">
<FrameLayout
@@ -34,6 +34,17 @@
android:layout_gravity="bottom|end"
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>
<TextView

View File

@@ -101,4 +101,11 @@
<item>@string/pages</item>
<item>@string/webtoon</item>
</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>

View File

@@ -664,4 +664,8 @@
<string name="sources_unpinned">Sources unpinned</string>
<string name="sources_pinned">Sources pinned</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>

View File

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