Show reverse progress and chapters in lists #904

This commit is contained in:
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 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(
oldManga: Manga,
newManga: Manga,
) { ) {
suspend operator fun invoke( val oldDetails =
oldManga: Manga, if (oldManga.chapters.isNullOrEmpty()) {
newManga: Manga, runCatchingCancellable {
) { mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga)
val oldDetails = }.getOrDefault(oldManga)
if (oldManga.chapters.isNullOrEmpty()) { } else {
runCatchingCancellable { oldManga
mangaRepositoryFactory.create(oldManga.source).getDetails(oldManga) }
}.getOrDefault(oldManga) val newDetails =
} else { if (newManga.chapters.isNullOrEmpty()) {
oldManga mangaRepositoryFactory.create(newManga.source).getDetails(newManga)
} } else {
val newDetails = newManga
if (newManga.chapters.isNullOrEmpty()) { }
mangaRepositoryFactory.create(newManga.source).getDetails(newManga) mangaDataRepository.storeManga(newDetails)
} else { database.withTransaction {
newManga // replace favorites
} val favoritesDao = database.getFavouritesDao()
mangaDataRepository.storeManga(newDetails) val oldFavourites = favoritesDao.findAllRaw(oldDetails.id)
database.withTransaction { if (oldFavourites.isNotEmpty()) {
// replace favorites favoritesDao.delete(oldManga.id)
val favoritesDao = database.getFavouritesDao() for (f in oldFavourites) {
val oldFavourites = favoritesDao.findAllRaw(oldDetails.id) val e =
if (oldFavourites.isNotEmpty()) { f.copy(
favoritesDao.delete(oldManga.id) mangaId = newManga.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,
) )
tracksDao.delete(oldDetails.id) favoritesDao.upsert(e)
tracksDao.upsert(newTrack)
} }
// scrobbling }
for (scrobbler in scrobblers) { // replace history
if (!scrobbler.isEnabled) { val historyDao = database.getHistoryDao()
continue val oldHistory = historyDao.find(oldDetails.id)
} val newHistory =
val prevInfo = scrobbler.getScrobblingInfoOrNull(oldDetails.id) ?: continue if (oldHistory != null) {
scrobbler.unregisterScrobbling(oldDetails.id) val newHistory = makeNewHistory(oldDetails, newDetails, oldHistory)
scrobbler.linkManga(newDetails.id, prevInfo.targetId) historyDao.delete(oldDetails.id)
scrobbler.updateScrobblingInfo( 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, mangaId = newDetails.id,
rating = prevInfo.rating, lastChapterId = lastChapter?.id ?: 0L,
status = newChapters = 0,
prevInfo.status ?: when { lastCheckTime = System.currentTimeMillis(),
newHistory == null -> ScrobblingStatus.PLANNED lastChapterDate = lastChapter?.uploadDate ?: 0L,
newHistory.percent == 1f -> ScrobblingStatus.COMPLETED lastResult = TrackEntity.RESULT_EXTERNAL_MODIFICATION,
else -> ScrobblingStatus.READING lastError = null,
},
comment = prevInfo.comment,
) )
if (newHistory != null) { tracksDao.delete(oldDetails.id)
scrobbler.scrobble( tracksDao.upsert(newTrack)
manga = newDetails,
chapterId = newHistory.chapterId,
)
}
}
} }
progressUpdateUseCase(newManga) // scrobbling
} for (scrobbler in scrobblers) {
if (!scrobbler.isEnabled) {
private fun makeNewHistory( continue
oldManga: Manga, }
newManga: Manga, val prevInfo = scrobbler.getScrobblingInfoOrNull(oldDetails.id) ?: continue
history: HistoryEntity, scrobbler.unregisterScrobbling(oldDetails.id)
): HistoryEntity { scrobbler.linkManga(newDetails.id, prevInfo.targetId)
if (oldManga.chapters.isNullOrEmpty()) { // probably broken manga/source scrobbler.updateScrobblingInfo(
val branch = newManga.getPreferredBranch(null) mangaId = newDetails.id,
val chapters = checkNotNull(newManga.getChapters(branch)) rating = prevInfo.rating,
val currentChapter = status =
if (history.percent in 0f..1f) { prevInfo.status ?: when {
chapters[(chapters.lastIndex * history.percent).toInt()] newHistory == null -> ScrobblingStatus.PLANNED
} else { newHistory.percent == 1f -> ScrobblingStatus.COMPLETED
chapters.first() else -> ScrobblingStatus.READING
} },
return HistoryEntity( comment = prevInfo.comment,
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,
) )
} if (newHistory != null) {
val branch = oldManga.getPreferredBranch(history.toMangaHistory()) scrobbler.scrobble(
val oldChapters = checkNotNull(oldManga.getChapters(branch)) manga = newDetails,
var index = oldChapters.indexOfFirst { it.id == history.chapterId } chapterId = newHistory.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)
} }
val newChapterId = }
checkNotNull(newChapters[newBranch]) }
.let { progressUpdateUseCase(newManga)
val oldChapter = oldChapters[index] }
it.findByNumber(oldChapter.volume, oldChapter.number) ?: it.getOrNull(index) ?: it.last()
}.id
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( return HistoryEntity(
mangaId = newManga.id, mangaId = newManga.id,
createdAt = history.createdAt, createdAt = history.createdAt,
updatedAt = System.currentTimeMillis(), updatedAt = System.currentTimeMillis(),
chapterId = newChapterId, chapterId = currentChapter.id,
page = history.page, page = history.page,
scroll = history.scroll, scroll = history.scroll,
percent = PROGRESS_NONE, percent = history.percent,
deletedAt = 0, deletedAt = 0,
chaptersCount = checkNotNull(newChapters[newBranch]).size, chaptersCount = chapters.count { it.branch == currentChapter.branch },
) )
} }
val branch = oldManga.getPreferredBranch(history.toMangaHistory())
private fun List<MangaChapter>.findByNumber( val oldChapters = checkNotNull(oldManga.getChapters(branch))
volume: Int, var index = oldChapters.indexOfFirst { it.id == history.chapterId }
number: Float, if (index < 0) {
): MangaChapter? = index =
if (number <= 0f) { if (history.percent in 0f..1f) {
null (oldChapters.lastIndex * history.percent).toInt()
} else {
0
}
}
val newChapters = checkNotNull(newManga.chapters).groupBy { it.branch }
val newBranch =
if (newChapters.containsKey(branch)) {
branch
} else { } 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 -> 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)

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

View File

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

View File

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

View File

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

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( override val content = combine(
mangaList, mangaList,
listMode, observeListModeWithTriggers(),
listError, listError,
) { list, mode, error -> ) { list, mode, error ->
when { 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.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,
) )
} }

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") @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)

View File

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

View File

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

View File

@@ -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,
) )

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.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() {

View File

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

View File

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

View File

@@ -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()

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.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

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

View File

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

View File

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

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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