Show reverse progress and chapters in lists #904
This commit is contained in:
@@ -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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user