Merge remote-tracking branch 'origin/devel' into devel
commit
6e5519419d
@ -0,0 +1,9 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
data class MangaSourceInfo(
|
||||||
|
val mangaSource: MangaSource,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
val isPinned: Boolean,
|
||||||
|
) : MangaSource by mangaSource
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package org.koitharu.kotatsu.core.model.parcelable
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import kotlinx.parcelize.Parceler
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
class MangaSourceParceler : Parceler<MangaSource> {
|
||||||
|
|
||||||
|
override fun create(parcel: Parcel): MangaSource = MangaSource(parcel.readString())
|
||||||
|
|
||||||
|
override fun MangaSource.write(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.MutableLongSet
|
||||||
|
import coil.request.CachePolicy
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
|
import org.koitharu.kotatsu.core.cache.SafeDeferred
|
||||||
|
import org.koitharu.kotatsu.core.util.MultiMutex
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
|
||||||
|
abstract class CachingMangaRepository(
|
||||||
|
private val cache: MemoryContentCache,
|
||||||
|
) : MangaRepository {
|
||||||
|
|
||||||
|
private val detailsMutex = MultiMutex<Long>()
|
||||||
|
private val relatedMangaMutex = MultiMutex<Long>()
|
||||||
|
private val pagesMutex = MultiMutex<Long>()
|
||||||
|
|
||||||
|
final override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
||||||
|
|
||||||
|
final override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
|
||||||
|
cache.getPages(source, chapter.url)?.let { return it }
|
||||||
|
val pages = asyncSafe {
|
||||||
|
getPagesImpl(chapter).distinctById()
|
||||||
|
}
|
||||||
|
cache.putPages(source, chapter.url, pages)
|
||||||
|
pages
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
final override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
|
||||||
|
cache.getRelatedManga(source, seed.url)?.let { return it }
|
||||||
|
val related = asyncSafe {
|
||||||
|
getRelatedMangaImpl(seed).filterNot { it.id == seed.id }
|
||||||
|
}
|
||||||
|
cache.putRelatedManga(source, seed.url, related)
|
||||||
|
related
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
|
||||||
|
if (cachePolicy.readEnabled) {
|
||||||
|
cache.getDetails(source, manga.url)?.let { return it }
|
||||||
|
}
|
||||||
|
val details = asyncSafe {
|
||||||
|
getDetailsImpl(manga)
|
||||||
|
}
|
||||||
|
if (cachePolicy.writeEnabled) {
|
||||||
|
cache.putDetails(source, manga.url, details)
|
||||||
|
}
|
||||||
|
details
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
suspend fun peekDetails(manga: Manga): Manga? {
|
||||||
|
return cache.getDetails(source, manga.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateCache() {
|
||||||
|
cache.clear(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract suspend fun getDetailsImpl(manga: Manga): Manga
|
||||||
|
|
||||||
|
protected abstract suspend fun getRelatedMangaImpl(seed: Manga): List<Manga>
|
||||||
|
|
||||||
|
protected abstract suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage>
|
||||||
|
|
||||||
|
private suspend fun <T> asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred<T> {
|
||||||
|
var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
|
||||||
|
if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
|
||||||
|
dispatcher = Dispatchers.Default
|
||||||
|
}
|
||||||
|
return SafeDeferred(
|
||||||
|
processLifecycleScope.async(dispatcher) {
|
||||||
|
runCatchingCancellable { block() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<MangaPage>.distinctById(): List<MangaPage> {
|
||||||
|
if (isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val result = ArrayList<MangaPage>(size)
|
||||||
|
val set = MutableLongSet(size)
|
||||||
|
for (page in this) {
|
||||||
|
if (set.add(page.id)) {
|
||||||
|
result.add(page)
|
||||||
|
} else if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(null, "Duplicate page: $page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,38 +1,49 @@
|
|||||||
package org.koitharu.kotatsu.core.parser
|
package org.koitharu.kotatsu.core.parser
|
||||||
|
|
||||||
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
|
||||||
import org.koitharu.kotatsu.parsers.config.ConfigKey
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
/**
|
class EmptyMangaRepository(override val source: MangaSource) : MangaRepository {
|
||||||
* This parser is just for parser development, it should not be used in releases
|
|
||||||
*/
|
|
||||||
class EmptyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
|
|
||||||
|
|
||||||
override val configKeyDomain: ConfigKey.Domain
|
override val sortOrders: Set<SortOrder>
|
||||||
get() = ConfigKey.Domain("localhost")
|
|
||||||
|
|
||||||
override val availableSortOrders: Set<SortOrder>
|
|
||||||
get() = EnumSet.allOf(SortOrder::class.java)
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
|
override val states: Set<MangaState>
|
||||||
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
get() = emptySet()
|
||||||
|
override val contentRatings: Set<ContentRating>
|
||||||
|
get() = emptySet()
|
||||||
|
override var defaultSortOrder: SortOrder
|
||||||
|
get() = SortOrder.NEWEST
|
||||||
|
set(value) = Unit
|
||||||
|
override val isMultipleTagsSupported: Boolean
|
||||||
|
get() = false
|
||||||
|
override val isTagsExclusionSupported: Boolean
|
||||||
|
get() = false
|
||||||
|
override val isSearchSupported: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = stub(null)
|
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
||||||
|
|
||||||
override suspend fun getAvailableTags(): Set<MangaTag> = stub(null)
|
override suspend fun getPageUrl(page: MangaPage): String = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getLocales(): Set<Locale> = stub(null)
|
||||||
|
|
||||||
override suspend fun getRelatedManga(seed: Manga): List<Manga> = stub(seed)
|
override suspend fun getRelated(seed: Manga): List<Manga> = stub(seed)
|
||||||
|
|
||||||
private fun stub(manga: Manga?): Nothing {
|
private fun stub(manga: Manga?): Nothing {
|
||||||
throw UnsupportedSourceException("This manga source is not supported", manga)
|
throw UnsupportedSourceException("This manga source is not supported", manga)
|
||||||
@ -0,0 +1,264 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser.external
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.database.Cursor
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
|
import org.koitharu.kotatsu.core.parser.CachingMangaRepository
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.parsers.util.find
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.splitTwoParts
|
||||||
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class ExternalMangaRepository(
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
override val source: ExternalMangaSource,
|
||||||
|
cache: MemoryContentCache,
|
||||||
|
) : CachingMangaRepository(cache) {
|
||||||
|
|
||||||
|
private val capabilities by lazy { queryCapabilities() }
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder>
|
||||||
|
get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL)
|
||||||
|
override val states: Set<MangaState>
|
||||||
|
get() = capabilities?.availableStates.orEmpty()
|
||||||
|
override val contentRatings: Set<ContentRating>
|
||||||
|
get() = capabilities?.availableContentRating.orEmpty()
|
||||||
|
override var defaultSortOrder: SortOrder
|
||||||
|
get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL
|
||||||
|
set(value) = Unit
|
||||||
|
override val isMultipleTagsSupported: Boolean
|
||||||
|
get() = capabilities?.isMultipleTagsSupported ?: true
|
||||||
|
override val isTagsExclusionSupported: Boolean
|
||||||
|
get() = capabilities?.isTagsExclusionSupported ?: false
|
||||||
|
override val isSearchSupported: Boolean
|
||||||
|
get() = capabilities?.isSearchSupported ?: true
|
||||||
|
|
||||||
|
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> =
|
||||||
|
runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/manga".toUri().buildUpon()
|
||||||
|
uri.appendQueryParameter("offset", offset.toString())
|
||||||
|
when (filter) {
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) }
|
||||||
|
filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) }
|
||||||
|
filter.states.forEach { uri.appendQueryParameter("state", it.name) }
|
||||||
|
filter.locale?.let { uri.appendQueryParameter("locale", it.language) }
|
||||||
|
filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
uri.appendQueryParameter("query", filter.query)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)?.use { cursor ->
|
||||||
|
val result = ArrayList<Manga>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += cursor.getManga()
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetailsImpl(manga: Manga): Manga = coroutineScope {
|
||||||
|
val chapters = async { queryChapters(manga.url) }
|
||||||
|
val details = queryDetails(manga.url)
|
||||||
|
Manga(
|
||||||
|
id = manga.id,
|
||||||
|
title = details.title.ifBlank { manga.title },
|
||||||
|
altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle },
|
||||||
|
url = details.url.ifEmpty { manga.url },
|
||||||
|
publicUrl = details.publicUrl.ifEmpty { manga.publicUrl },
|
||||||
|
rating = maxOf(details.rating, manga.rating),
|
||||||
|
isNsfw = details.isNsfw,
|
||||||
|
coverUrl = details.coverUrl.ifEmpty { manga.coverUrl },
|
||||||
|
tags = details.tags + manga.tags,
|
||||||
|
state = details.state ?: manga.state,
|
||||||
|
author = details.author.ifNullOrEmpty { manga.author },
|
||||||
|
largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl },
|
||||||
|
description = details.description.ifNullOrEmpty { manga.description },
|
||||||
|
chapters = chapters.await(),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/chapters".toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(chapter.url)
|
||||||
|
.build()
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
val result = ArrayList<MangaPage>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += MangaPage(
|
||||||
|
id = cursor.getLong(0),
|
||||||
|
url = cursor.getString(1),
|
||||||
|
preview = cursor.getStringOrNull(2),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPageUrl(page: MangaPage): String = page.url
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/tags".toUri()
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
val result = ArraySet<MangaTag>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += MangaTag(
|
||||||
|
key = cursor.getString(0),
|
||||||
|
title = cursor.getString(1),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLocales(): Set<Locale> = emptySet()
|
||||||
|
|
||||||
|
override suspend fun getRelatedMangaImpl(seed: Manga): List<Manga> = emptyList() // TODO
|
||||||
|
|
||||||
|
private suspend fun queryDetails(url: String): Manga = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/manga".toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(url)
|
||||||
|
.build()
|
||||||
|
checkNotNull(
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.getManga()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun queryChapters(url: String): List<MangaChapter>? = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/manga/chapters".toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(url)
|
||||||
|
.build()
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
val result = ArrayList<MangaChapter>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += MangaChapter(
|
||||||
|
id = cursor.getLong(0),
|
||||||
|
name = cursor.getString(1),
|
||||||
|
number = cursor.getFloat(2),
|
||||||
|
volume = cursor.getInt(3),
|
||||||
|
url = cursor.getString(4),
|
||||||
|
scanlator = cursor.getStringOrNull(5),
|
||||||
|
uploadDate = cursor.getLong(6),
|
||||||
|
branch = cursor.getStringOrNull(7),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getManga() = Manga(
|
||||||
|
id = getLong(0),
|
||||||
|
title = getString(1),
|
||||||
|
altTitle = getStringOrNull(2),
|
||||||
|
url = getString(3),
|
||||||
|
publicUrl = getString(4),
|
||||||
|
rating = getFloat(5),
|
||||||
|
isNsfw = getInt(6) > 1,
|
||||||
|
coverUrl = getString(7),
|
||||||
|
tags = getStringOrNull(8)?.split(':')?.mapNotNullToSet {
|
||||||
|
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
|
||||||
|
MangaTag(key = parts.first, title = parts.second, source = source)
|
||||||
|
}.orEmpty(),
|
||||||
|
state = getStringOrNull(9)?.let { MangaState.entries.find(it) },
|
||||||
|
author = optString(10),
|
||||||
|
largeCoverUrl = optString(11),
|
||||||
|
description = optString(12),
|
||||||
|
chapters = emptyList(),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Cursor.optString(columnIndex: Int): String? {
|
||||||
|
return if (isNull(columnIndex)) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
getString(columnIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryCapabilities(): MangaSourceCapabilities? {
|
||||||
|
val uri = "content://${source.authority}/capabilities".toUri()
|
||||||
|
return contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
MangaSourceCapabilities(
|
||||||
|
availableSortOrders = cursor.getStringOrNull(0)
|
||||||
|
?.split(',')
|
||||||
|
?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) {
|
||||||
|
SortOrder.entries.find(it)
|
||||||
|
}.orEmpty(),
|
||||||
|
availableStates = cursor.getStringOrNull(1)
|
||||||
|
?.split(',')
|
||||||
|
?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) {
|
||||||
|
MangaState.entries.find(it)
|
||||||
|
}.orEmpty(),
|
||||||
|
availableContentRating = cursor.getStringOrNull(2)
|
||||||
|
?.split(',')
|
||||||
|
?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) {
|
||||||
|
ContentRating.entries.find(it)
|
||||||
|
}.orEmpty(),
|
||||||
|
isMultipleTagsSupported = cursor.getInt(3) > 1,
|
||||||
|
isTagsExclusionSupported = cursor.getInt(4) > 1,
|
||||||
|
isSearchSupported = cursor.getInt(5) > 1,
|
||||||
|
contentType = ContentType.entries.find(cursor.getString(6)) ?: ContentType.OTHER,
|
||||||
|
defaultSortOrder = cursor.getStringOrNull(7)?.let {
|
||||||
|
SortOrder.entries.find(it)
|
||||||
|
} ?: SortOrder.ALPHABETICAL,
|
||||||
|
sourceLocale = cursor.getStringOrNull(8)?.let { Locale(it) } ?: Locale.ROOT,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MangaSourceCapabilities(
|
||||||
|
val availableSortOrders: Set<SortOrder>,
|
||||||
|
val availableStates: Set<MangaState>,
|
||||||
|
val availableContentRating: Set<ContentRating>,
|
||||||
|
val isMultipleTagsSupported: Boolean,
|
||||||
|
val isTagsExclusionSupported: Boolean,
|
||||||
|
val isSearchSupported: Boolean,
|
||||||
|
val contentType: ContentType,
|
||||||
|
val defaultSortOrder: SortOrder,
|
||||||
|
val sourceLocale: Locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package org.koitharu.kotatsu.core.parser.external
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
data class ExternalMangaSource(
|
||||||
|
val packageName: String,
|
||||||
|
val authority: String,
|
||||||
|
) : MangaSource {
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = "content:$packageName/$authority"
|
||||||
|
|
||||||
|
private var cachedName: String? = null
|
||||||
|
|
||||||
|
fun isAvailable(context: Context): Boolean {
|
||||||
|
return context.packageManager.resolveContentProvider(authority, 0)?.isEnabled == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolveName(context: Context): String {
|
||||||
|
cachedName?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
val pm = context.packageManager
|
||||||
|
val info = pm.resolveContentProvider(authority, 0)
|
||||||
|
return info?.loadLabel(pm)?.toString()?.also {
|
||||||
|
cachedName = it
|
||||||
|
} ?: authority
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
package org.koitharu.kotatsu.core.prefs
|
||||||
|
|
||||||
|
enum class ProgressIndicatorMode {
|
||||||
|
|
||||||
|
NONE, PERCENT_READ, PERCENT_LEFT, CHAPTERS_READ, CHAPTERS_LEFT;
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package org.koitharu.kotatsu.core.ui.image
|
||||||
|
|
||||||
|
import android.animation.TimeAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.drawable.Animatable
|
||||||
|
import androidx.annotation.StyleRes
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
|
import com.google.android.material.animation.ArgbEvaluatorCompat
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.util.KotatsuColors
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.getAnimationDuration
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
class AnimatedFaviconDrawable(
|
||||||
|
context: Context,
|
||||||
|
@StyleRes styleResId: Int,
|
||||||
|
name: String,
|
||||||
|
) : FaviconDrawable(context, styleResId, name), Animatable, TimeAnimator.TimeListener {
|
||||||
|
|
||||||
|
private val interpolator = FastOutSlowInInterpolator()
|
||||||
|
private val period = context.getAnimationDuration(R.integer.config_longAnimTime) * 2
|
||||||
|
private val timeAnimator = TimeAnimator()
|
||||||
|
|
||||||
|
private val colorHigh = MaterialColors.harmonize(KotatsuColors.random(name), colorBackground)
|
||||||
|
private val colorLow = ArgbEvaluatorCompat.getInstance().evaluate(0.3f, colorHigh, colorBackground)
|
||||||
|
|
||||||
|
init {
|
||||||
|
timeAnimator.setTimeListener(this)
|
||||||
|
updateColor()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(canvas: Canvas) {
|
||||||
|
if (!isRunning && period > 0) {
|
||||||
|
updateColor()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
super.draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setAlpha(alpha: Int) = Unit
|
||||||
|
|
||||||
|
override fun getAlpha(): Int = 255
|
||||||
|
|
||||||
|
override fun onTimeUpdate(animation: TimeAnimator?, totalTime: Long, deltaTime: Long) {
|
||||||
|
callback?.also {
|
||||||
|
updateColor()
|
||||||
|
it.invalidateDrawable(this)
|
||||||
|
} ?: stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun start() {
|
||||||
|
timeAnimator.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
timeAnimator.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isRunning(): Boolean = timeAnimator.isStarted
|
||||||
|
|
||||||
|
private fun updateColor() {
|
||||||
|
if (period <= 0f) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val ph = period / 2
|
||||||
|
val fraction = abs((System.currentTimeMillis() % period) - ph) / ph.toFloat()
|
||||||
|
colorForeground = ArgbEvaluatorCompat.getInstance()
|
||||||
|
.evaluate(interpolator.getInterpolation(fraction), colorLow, colorHigh)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,12 +1,10 @@
|
|||||||
package org.koitharu.kotatsu.favourites.domain.model
|
package org.koitharu.kotatsu.favourites.domain.model
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.find
|
|
||||||
|
|
||||||
data class Cover(
|
data class Cover(
|
||||||
val url: String,
|
val url: String,
|
||||||
val source: String,
|
val source: String,
|
||||||
) {
|
) {
|
||||||
val mangaSource: MangaSource?
|
val mangaSource by lazy { MangaSource(source) }
|
||||||
get() = if (source.isEmpty()) null else MangaSource.entries.find(source)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.domain
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.ColorRes
|
|
||||||
import dagger.Reusable
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.history.data.HistoryRepository
|
|
||||||
import org.koitharu.kotatsu.history.data.PROGRESS_NONE
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@Reusable
|
|
||||||
class ListExtraProvider @Inject constructor(
|
|
||||||
@ApplicationContext context: Context,
|
|
||||||
private val settings: AppSettings,
|
|
||||||
private val trackingRepository: TrackingRepository,
|
|
||||||
private val historyRepository: HistoryRepository,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val dict by lazy {
|
|
||||||
context.resources.openRawResource(R.raw.tags_redlist).use {
|
|
||||||
val set = HashSet<String>()
|
|
||||||
it.bufferedReader().forEachLine { x ->
|
|
||||||
val line = x.trim()
|
|
||||||
if (line.isNotEmpty()) {
|
|
||||||
set.add(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getCounter(mangaId: Long): Int {
|
|
||||||
return if (settings.isTrackerEnabled) {
|
|
||||||
trackingRepository.getNewChaptersCount(mangaId)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getProgress(mangaId: Long): Float {
|
|
||||||
return if (settings.isReadingIndicatorsEnabled) {
|
|
||||||
historyRepository.getProgress(mangaId)
|
|
||||||
} else {
|
|
||||||
PROGRESS_NONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorRes
|
|
||||||
fun getTagTint(tag: MangaTag): Int {
|
|
||||||
return if (tag.title.lowercase() in dict) {
|
|
||||||
R.color.warning
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,131 @@
|
|||||||
|
package org.koitharu.kotatsu.list.domain
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.collection.MutableScatterSet
|
||||||
|
import androidx.collection.ScatterSet
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
|
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||||
|
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||||
|
import org.koitharu.kotatsu.history.data.HistoryRepository
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaCompactListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaDetailedListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaGridModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.MangaListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class MangaListMapper @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
|
private val settings: AppSettings,
|
||||||
|
private val trackingRepository: TrackingRepository,
|
||||||
|
private val historyRepository: HistoryRepository,
|
||||||
|
private val favouritesRepository: FavouritesRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val dict by lazy { readTagsDict(context) }
|
||||||
|
|
||||||
|
suspend fun toListModelList(manga: Collection<Manga>, mode: ListMode): List<MangaListModel> = manga.map {
|
||||||
|
toListModel(it, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun toListModelList(
|
||||||
|
destination: MutableCollection<in MangaListModel>,
|
||||||
|
manga: Collection<Manga>,
|
||||||
|
mode: ListMode
|
||||||
|
) = manga.mapTo(destination) {
|
||||||
|
toListModel(it, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun toListModel(manga: Manga, mode: ListMode): MangaListModel = when (mode) {
|
||||||
|
ListMode.LIST -> toCompactListModel(manga)
|
||||||
|
ListMode.DETAILED_LIST -> toDetailedListModel(manga)
|
||||||
|
ListMode.GRID -> toGridModel(manga)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun toCompactListModel(manga: Manga) = MangaCompactListModel(
|
||||||
|
id = manga.id,
|
||||||
|
title = manga.title,
|
||||||
|
subtitle = manga.tags.joinToString(", ") { it.title },
|
||||||
|
coverUrl = manga.coverUrl,
|
||||||
|
manga = manga,
|
||||||
|
counter = getCounter(manga.id),
|
||||||
|
progress = getProgress(manga.id),
|
||||||
|
isFavorite = isFavorite(manga.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun toDetailedListModel(manga: Manga) = MangaDetailedListModel(
|
||||||
|
id = manga.id,
|
||||||
|
title = manga.title,
|
||||||
|
subtitle = manga.altTitle,
|
||||||
|
coverUrl = manga.coverUrl,
|
||||||
|
manga = manga,
|
||||||
|
counter = getCounter(manga.id),
|
||||||
|
progress = getProgress(manga.id),
|
||||||
|
isFavorite = isFavorite(manga.id),
|
||||||
|
tags = mapTags(manga.tags),
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun toGridModel(manga: Manga) = MangaGridModel(
|
||||||
|
id = manga.id,
|
||||||
|
title = manga.title,
|
||||||
|
coverUrl = manga.coverUrl,
|
||||||
|
manga = manga,
|
||||||
|
counter = getCounter(manga.id),
|
||||||
|
progress = getProgress(manga.id),
|
||||||
|
isFavorite = isFavorite(manga.id),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun mapTags(tags: Collection<MangaTag>) = tags.map {
|
||||||
|
ChipsView.ChipModel(
|
||||||
|
tint = getTagTint(it),
|
||||||
|
title = it.title,
|
||||||
|
data = it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getCounter(mangaId: Long): Int {
|
||||||
|
return if (settings.isTrackerEnabled) {
|
||||||
|
trackingRepository.getNewChaptersCount(mangaId)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getProgress(mangaId: Long): ReadingProgress? {
|
||||||
|
return historyRepository.getProgress(mangaId, settings.progressIndicatorMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isFavorite(mangaId: Long): Boolean {
|
||||||
|
return false // TODO favouritesRepository.isFavorite(mangaId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorRes
|
||||||
|
private fun getTagTint(tag: MangaTag): Int {
|
||||||
|
return if (tag.title.lowercase() in dict) {
|
||||||
|
R.color.warning
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readTagsDict(context: Context): ScatterSet<String> =
|
||||||
|
context.resources.openRawResource(R.raw.tags_redlist).use {
|
||||||
|
val set = MutableScatterSet<String>()
|
||||||
|
it.bufferedReader().forEachLine { x ->
|
||||||
|
val line = x.trim()
|
||||||
|
if (line.isNotEmpty()) {
|
||||||
|
set.add(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set.trim()
|
||||||
|
set
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package org.koitharu.kotatsu.list.ui.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.list.domain.ReadingProgress
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
|
data class MangaCompactListModel(
|
||||||
|
override val id: Long,
|
||||||
|
override val title: String,
|
||||||
|
val subtitle: String,
|
||||||
|
override val coverUrl: String,
|
||||||
|
override val manga: Manga,
|
||||||
|
override val counter: Int,
|
||||||
|
override val progress: ReadingProgress?,
|
||||||
|
override val isFavorite: Boolean,
|
||||||
|
) : MangaListModel()
|
||||||
@ -1,15 +1,17 @@
|
|||||||
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 MangaListDetailedModel(
|
data class MangaDetailedListModel(
|
||||||
override val id: Long,
|
override val id: Long,
|
||||||
override val title: String,
|
override val title: String,
|
||||||
val subtitle: String?,
|
val subtitle: String?,
|
||||||
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>,
|
||||||
) : MangaItemModel()
|
) : MangaListModel()
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.list.ui.model
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
sealed class MangaItemModel : ListModel {
|
|
||||||
|
|
||||||
abstract val id: Long
|
|
||||||
abstract val manga: Manga
|
|
||||||
abstract val title: String
|
|
||||||
abstract val coverUrl: String
|
|
||||||
abstract val counter: Int
|
|
||||||
abstract val progress: Float
|
|
||||||
|
|
||||||
val source: MangaSource
|
|
||||||
get() = manga.source
|
|
||||||
|
|
||||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
|
||||||
return other is MangaItemModel && other.javaClass == javaClass && id == other.id
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(previousState: ListModel): Any? {
|
|
||||||
return when {
|
|
||||||
previousState !is MangaItemModel -> super.getChangePayload(previousState)
|
|
||||||
progress != previousState.progress -> ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED
|
|
||||||
counter != previousState.counter -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +1,34 @@
|
|||||||
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.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
|
||||||
|
|
||||||
data class MangaListModel(
|
sealed class MangaListModel : ListModel {
|
||||||
override val id: Long,
|
|
||||||
override val title: String,
|
abstract val id: Long
|
||||||
val subtitle: String,
|
abstract val manga: Manga
|
||||||
override val coverUrl: String,
|
abstract val title: String
|
||||||
override val manga: Manga,
|
abstract val coverUrl: String
|
||||||
override val counter: Int,
|
abstract val counter: Int
|
||||||
override val progress: Float,
|
abstract val isFavorite: Boolean
|
||||||
) : MangaItemModel()
|
abstract val progress: ReadingProgress?
|
||||||
|
|
||||||
|
val source: MangaSource
|
||||||
|
get() = manga.source
|
||||||
|
|
||||||
|
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||||
|
return other is MangaListModel && other.javaClass == javaClass && id == other.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(previousState: ListModel): Any? = when {
|
||||||
|
previousState !is MangaListModel || previousState.manga != manga -> null
|
||||||
|
|
||||||
|
previousState.progress != progress -> PAYLOAD_PROGRESS_CHANGED
|
||||||
|
previousState.isFavorite != isFavorite || previousState.counter != counter -> PAYLOAD_ANYTHING_CHANGED
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue