Fix loading empty manga

devel
Koitharu 6 months ago
parent 9fde0106be
commit 5590ab7c8a
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -0,0 +1,10 @@
package org.koitharu.kotatsu.core.exceptions
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
import org.koitharu.kotatsu.parsers.model.Manga
class EmptyMangaException(
val reason: EmptyMangaReason?,
val manga: Manga,
cause: Throwable?
) : IllegalStateException(cause)

@ -16,6 +16,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyMangaException
import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException
import org.koitharu.kotatsu.core.exceptions.ProxyConfigException import org.koitharu.kotatsu.core.exceptions.ProxyConfigException
import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException import org.koitharu.kotatsu.core.exceptions.UnsupportedSourceException
@ -25,6 +26,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
import org.koitharu.kotatsu.core.util.ext.isHttpUrl import org.koitharu.kotatsu.core.util.ext.isHttpUrl
import org.koitharu.kotatsu.core.util.ext.restartApplication import org.koitharu.kotatsu.core.util.ext.restartApplication
import org.koitharu.kotatsu.details.ui.pager.EmptyMangaReason
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -83,6 +85,16 @@ class ExceptionResolver private constructor(
false false
} }
is EmptyMangaException -> {
when (e.reason) {
EmptyMangaReason.NO_CHAPTERS -> openAlternatives(e.manga)
EmptyMangaReason.LOADING_ERROR -> Unit
EmptyMangaReason.RESTRICTED -> host.router.openBrowser(e.manga)
else -> Unit
}
false
}
is UnsupportedSourceException -> { is UnsupportedSourceException -> {
e.manga?.let { openAlternatives(it) } e.manga?.let { openAlternatives(it) }
false false
@ -229,6 +241,12 @@ class ExceptionResolver private constructor(
is InteractiveActionRequiredException -> R.string._continue is InteractiveActionRequiredException -> R.string._continue
is EmptyMangaException -> when (e.reason) {
EmptyMangaReason.RESTRICTED -> if (e.manga.publicUrl.isHttpUrl()) R.string.open_in_browser else 0
EmptyMangaReason.NO_CHAPTERS -> R.string.alternatives
else -> 0
}
else -> 0 else -> 0
} }

@ -22,6 +22,7 @@ import org.koitharu.kotatsu.core.exceptions.CaughtException
import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException import org.koitharu.kotatsu.core.exceptions.CloudFlareBlockedException
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.EmptyMangaException
import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException import org.koitharu.kotatsu.core.exceptions.IncompatiblePluginException
import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException import org.koitharu.kotatsu.core.exceptions.InteractiveActionRequiredException
import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException import org.koitharu.kotatsu.core.exceptions.NoDataReceivedException
@ -62,216 +63,219 @@ private const val IMAGE_FORMAT_NOT_SUPPORTED = "Image format not supported"
private val FNFE_MESSAGE_REGEX = Regex("^(/[^\\s:]+)?.+?\\s([A-Z]{2,6})?\\s.+$") private val FNFE_MESSAGE_REGEX = Regex("^(/[^\\s:]+)?.+?\\s([A-Z]{2,6})?\\s.+$")
fun Throwable.getDisplayMessage(resources: Resources): String = getDisplayMessageOrNull(resources) fun Throwable.getDisplayMessage(resources: Resources): String = getDisplayMessageOrNull(resources)
?: resources.getString(R.string.error_occurred) ?: resources.getString(R.string.error_occurred)
private fun Throwable.getDisplayMessageOrNull(resources: Resources): String? = when (this) { private fun Throwable.getDisplayMessageOrNull(resources: Resources): String? = when (this) {
is CancellationException -> cause?.getDisplayMessageOrNull(resources) ?: message is CancellationException -> cause?.getDisplayMessageOrNull(resources) ?: message
is CaughtException -> cause.getDisplayMessageOrNull(resources) is CaughtException -> cause.getDisplayMessageOrNull(resources)
is WrapperIOException -> cause.getDisplayMessageOrNull(resources) is WrapperIOException -> cause.getDisplayMessageOrNull(resources)
is ScrobblerAuthRequiredException -> resources.getString( is ScrobblerAuthRequiredException -> resources.getString(
R.string.scrobbler_auth_required, R.string.scrobbler_auth_required,
resources.getString(scrobbler.titleResId), resources.getString(scrobbler.titleResId),
) )
is AuthRequiredException -> resources.getString(R.string.auth_required) is AuthRequiredException -> resources.getString(R.string.auth_required)
is InteractiveActionRequiredException -> resources.getString(R.string.additional_action_required) is InteractiveActionRequiredException -> resources.getString(R.string.additional_action_required)
is CloudFlareProtectedException -> resources.getString(R.string.captcha_required_message) is CloudFlareProtectedException -> resources.getString(R.string.captcha_required_message)
is CloudFlareBlockedException -> resources.getString(R.string.blocked_by_server_message) is CloudFlareBlockedException -> resources.getString(R.string.blocked_by_server_message)
is ActivityNotFoundException, is ActivityNotFoundException,
is UnsupportedOperationException, is UnsupportedOperationException,
-> resources.getString(R.string.operation_not_supported) -> resources.getString(R.string.operation_not_supported)
is TooManyRequestExceptions -> { is TooManyRequestExceptions -> {
val delay = getRetryDelay() val delay = getRetryDelay()
val formattedTime = if (delay > 0L && delay < Long.MAX_VALUE) { val formattedTime = if (delay > 0L && delay < Long.MAX_VALUE) {
resources.formatDurationShort(delay) resources.formatDurationShort(delay)
} else { } else {
null null
} }
if (formattedTime != null) { if (formattedTime != null) {
resources.getString(R.string.too_many_requests_message_retry, formattedTime) resources.getString(R.string.too_many_requests_message_retry, formattedTime)
} else { } else {
resources.getString(R.string.too_many_requests_message) resources.getString(R.string.too_many_requests_message)
} }
} }
is ZipException -> resources.getString(R.string.error_corrupted_zip, this.message.orEmpty()) is ZipException -> resources.getString(R.string.error_corrupted_zip, this.message.orEmpty())
is SQLiteFullException -> resources.getString(R.string.error_no_space_left) is SQLiteFullException -> resources.getString(R.string.error_no_space_left)
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is BadBackupFormatException -> resources.getString(R.string.unsupported_backup_message) is BadBackupFormatException -> resources.getString(R.string.unsupported_backup_message)
is FileNotFoundException -> parseMessage(resources) ?: message is FileNotFoundException -> parseMessage(resources) ?: message
is AccessDeniedException -> resources.getString(R.string.no_access_to_file) is AccessDeniedException -> resources.getString(R.string.no_access_to_file)
is NonFileUriException -> resources.getString(R.string.error_non_file_uri) is NonFileUriException -> resources.getString(R.string.error_non_file_uri)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty) is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is ProxyConfigException -> resources.getString(R.string.invalid_proxy_configuration) is EmptyMangaException -> reason?.let { resources.getString(it.msgResId) } ?: cause?.getDisplayMessage(resources)
is SyncApiException, is ProxyConfigException -> resources.getString(R.string.invalid_proxy_configuration)
is ContentUnavailableException -> message is SyncApiException,
is ContentUnavailableException -> message
is ParseException -> shortMessage is ParseException -> shortMessage
is ConnectException, is ConnectException,
is UnknownHostException, is UnknownHostException,
is NoRouteToHostException, is NoRouteToHostException,
is SocketTimeoutException -> resources.getString(R.string.network_error) is SocketTimeoutException -> resources.getString(R.string.network_error)
is ImageDecodeException -> { is ImageDecodeException -> {
val type = format?.substringBefore('/') val type = format?.substringBefore('/')
val formatString = format.ifNullOrEmpty { resources.getString(R.string.unknown).lowercase(Locale.getDefault()) } val formatString = format.ifNullOrEmpty { resources.getString(R.string.unknown).lowercase(Locale.getDefault()) }
if (type.isNullOrEmpty() || type == "image") { if (type.isNullOrEmpty() || type == "image") {
resources.getString(R.string.error_image_format, formatString) resources.getString(R.string.error_image_format, formatString)
} else { } else {
resources.getString(R.string.error_not_image, formatString) resources.getString(R.string.error_not_image, formatString)
} }
} }
is NoDataReceivedException -> resources.getString(R.string.error_no_data_received) is NoDataReceivedException -> resources.getString(R.string.error_no_data_received)
is IncompatiblePluginException -> { is IncompatiblePluginException -> {
cause?.getDisplayMessageOrNull(resources)?.let { cause?.getDisplayMessageOrNull(resources)?.let {
resources.getString(R.string.plugin_incompatible_with_cause, it) resources.getString(R.string.plugin_incompatible_with_cause, it)
} ?: resources.getString(R.string.plugin_incompatible) } ?: resources.getString(R.string.plugin_incompatible)
} }
is WrongPasswordException -> resources.getString(R.string.wrong_password) is WrongPasswordException -> resources.getString(R.string.wrong_password)
is NotFoundException -> resources.getString(R.string.not_found_404) is NotFoundException -> resources.getString(R.string.not_found_404)
is UnsupportedSourceException -> resources.getString(R.string.unsupported_source) is UnsupportedSourceException -> resources.getString(R.string.unsupported_source)
is HttpException -> getHttpDisplayMessage(response.code, resources) is HttpException -> getHttpDisplayMessage(response.code, resources)
is HttpStatusException -> getHttpDisplayMessage(statusCode, resources) is HttpStatusException -> getHttpDisplayMessage(statusCode, resources)
else -> mapDisplayMessage(message, resources) ?: message else -> mapDisplayMessage(message, resources) ?: message
}.takeUnless { it.isNullOrBlank() } }.takeUnless { it.isNullOrBlank() }
@DrawableRes @DrawableRes
fun Throwable.getDisplayIcon(): Int = when (this) { fun Throwable.getDisplayIcon(): Int = when (this) {
is AuthRequiredException -> R.drawable.ic_auth_key_large is AuthRequiredException -> R.drawable.ic_auth_key_large
is CloudFlareProtectedException -> R.drawable.ic_bot_large is CloudFlareProtectedException -> R.drawable.ic_bot_large
is UnknownHostException, is UnknownHostException,
is SocketTimeoutException, is SocketTimeoutException,
is ConnectException, is ConnectException,
is NoRouteToHostException, is NoRouteToHostException,
is ProtocolException -> R.drawable.ic_plug_large is ProtocolException -> R.drawable.ic_plug_large
is CloudFlareBlockedException -> R.drawable.ic_denied_large is CloudFlareBlockedException -> R.drawable.ic_denied_large
is InteractiveActionRequiredException -> R.drawable.ic_interaction_large is InteractiveActionRequiredException -> R.drawable.ic_interaction_large
else -> R.drawable.ic_error_large else -> R.drawable.ic_error_large
} }
fun Throwable.getCauseUrl(): String? = when (this) { fun Throwable.getCauseUrl(): String? = when (this) {
is ParseException -> url is ParseException -> url
is NotFoundException -> url is NotFoundException -> url
is TooManyRequestExceptions -> url is TooManyRequestExceptions -> url
is CaughtException -> cause.getCauseUrl() is CaughtException -> cause.getCauseUrl()
is WrapperIOException -> cause.getCauseUrl() is WrapperIOException -> cause.getCauseUrl()
is NoDataReceivedException -> url is NoDataReceivedException -> url
is CloudFlareBlockedException -> url is CloudFlareBlockedException -> url
is CloudFlareProtectedException -> url is CloudFlareProtectedException -> url
is InteractiveActionRequiredException -> url is InteractiveActionRequiredException -> url
is HttpStatusException -> url is HttpStatusException -> url
is HttpException -> (response.delegate as? Response)?.request?.url?.toString() is UnsupportedSourceException -> manga?.publicUrl?.takeIf { it.isHttpUrl() }
else -> null is EmptyMangaException -> manga.publicUrl.takeIf { it.isHttpUrl() }
is HttpException -> (response.delegate as? Response)?.request?.url?.toString()
else -> null
} }
private fun getHttpDisplayMessage(statusCode: Int, resources: Resources): String? = when (statusCode) { private fun getHttpDisplayMessage(statusCode: Int, resources: Resources): String? = when (statusCode) {
HttpURLConnection.HTTP_NOT_FOUND -> resources.getString(R.string.not_found_404) HttpURLConnection.HTTP_NOT_FOUND -> resources.getString(R.string.not_found_404)
HttpURLConnection.HTTP_FORBIDDEN -> resources.getString(R.string.access_denied_403) HttpURLConnection.HTTP_FORBIDDEN -> resources.getString(R.string.access_denied_403)
HttpURLConnection.HTTP_GATEWAY_TIMEOUT -> resources.getString(R.string.network_unavailable) HttpURLConnection.HTTP_GATEWAY_TIMEOUT -> resources.getString(R.string.network_unavailable)
in 500..599 -> resources.getString(R.string.server_error, statusCode) in 500..599 -> resources.getString(R.string.server_error, statusCode)
else -> null else -> null
} }
private fun mapDisplayMessage(msg: String?, resources: Resources): String? = when { private fun mapDisplayMessage(msg: String?, resources: Resources): String? = when {
msg.isNullOrEmpty() -> null msg.isNullOrEmpty() -> null
msg.contains(MSG_NO_SPACE_LEFT) -> resources.getString(R.string.error_no_space_left) msg.contains(MSG_NO_SPACE_LEFT) -> resources.getString(R.string.error_no_space_left)
msg.contains(IMAGE_FORMAT_NOT_SUPPORTED) -> resources.getString(R.string.error_corrupted_file) msg.contains(IMAGE_FORMAT_NOT_SUPPORTED) -> resources.getString(R.string.error_corrupted_file)
msg == MSG_CONNECTION_RESET -> resources.getString(R.string.error_connection_reset) msg == MSG_CONNECTION_RESET -> resources.getString(R.string.error_connection_reset)
msg == FILTER_MULTIPLE_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_genres_not_supported) msg == FILTER_MULTIPLE_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_genres_not_supported)
msg == FILTER_MULTIPLE_STATES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_states_not_supported) msg == FILTER_MULTIPLE_STATES_NOT_SUPPORTED -> resources.getString(R.string.error_multiple_states_not_supported)
msg == SEARCH_NOT_SUPPORTED -> resources.getString(R.string.error_search_not_supported) msg == SEARCH_NOT_SUPPORTED -> resources.getString(R.string.error_search_not_supported)
msg == FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_filter_locale_genre_not_supported) msg == FILTER_BOTH_LOCALE_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_filter_locale_genre_not_supported)
msg == FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_filter_states_genre_not_supported) msg == FILTER_BOTH_STATES_GENRES_NOT_SUPPORTED -> resources.getString(R.string.error_filter_states_genre_not_supported)
else -> null else -> null
} }
fun Throwable.isReportable(): Boolean { fun Throwable.isReportable(): Boolean {
if (this is Error) { if (this is Error) {
return true return true
} }
if (this is CaughtException) { if (this is CaughtException) {
return cause.isReportable() return cause.isReportable()
} }
if (this is WrapperIOException) { if (this is WrapperIOException) {
return cause.isReportable() return cause.isReportable()
} }
if (ExceptionResolver.canResolve(this)) { if (ExceptionResolver.canResolve(this)) {
return false return false
} }
if (this is ParseException if (this is ParseException
|| this.isNetworkError() || this.isNetworkError()
|| this is CloudFlareBlockedException || this is CloudFlareBlockedException
|| this is CloudFlareProtectedException || this is CloudFlareProtectedException
|| this is BadBackupFormatException || this is BadBackupFormatException
|| this is WrongPasswordException || this is WrongPasswordException
|| this is TooManyRequestExceptions || this is TooManyRequestExceptions
|| this is HttpStatusException || this is HttpStatusException
) { ) {
return false return false
} }
return true return true
} }
fun Throwable.isNetworkError(): Boolean { fun Throwable.isNetworkError(): Boolean {
return this is UnknownHostException return this is UnknownHostException
|| this is SocketTimeoutException || this is SocketTimeoutException
|| this is StreamResetException || this is StreamResetException
|| this is SocketException || this is SocketException
|| this is HttpException && response.code == HttpURLConnection.HTTP_GATEWAY_TIMEOUT || this is HttpException && response.code == HttpURLConnection.HTTP_GATEWAY_TIMEOUT
} }
fun Throwable.report(silent: Boolean = false) { fun Throwable.report(silent: Boolean = false) {
val exception = CaughtException(this) val exception = CaughtException(this)
if (!silent) { if (!silent) {
exception.sendWithAcra() exception.sendWithAcra()
} else if (!BuildConfig.DEBUG) { } else if (!BuildConfig.DEBUG) {
exception.sendSilentlyWithAcra() exception.sendSilentlyWithAcra()
} }
} }
fun Throwable.isWebViewUnavailable(): Boolean { fun Throwable.isWebViewUnavailable(): Boolean {
val trace = stackTraceToString() val trace = stackTraceToString()
return trace.contains("android.webkit.WebView.<init>") return trace.contains("android.webkit.WebView.<init>")
} }
@Suppress("FunctionName") @Suppress("FunctionName")
fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT) fun NoSpaceLeftException() = IOException(MSG_NO_SPACE_LEFT)
fun FileNotFoundException.getFile(): File? { fun FileNotFoundException.getFile(): File? {
val groups = FNFE_MESSAGE_REGEX.matchEntire(message ?: return null)?.groupValues ?: return null val groups = FNFE_MESSAGE_REGEX.matchEntire(message ?: return null)?.groupValues ?: return null
return groups.getOrNull(1)?.let { File(it) } return groups.getOrNull(1)?.let { File(it) }
} }
fun FileNotFoundException.parseMessage(resources: Resources): String? { fun FileNotFoundException.parseMessage(resources: Resources): String? {
/* /*
Examples: Examples:
/storage/0000-0000/Android/media/d1f08350-0c25-460b-8f50-008e49de3873.jpg.tmp: open failed: EROFS (Read-only file system) /storage/0000-0000/Android/media/d1f08350-0c25-460b-8f50-008e49de3873.jpg.tmp: open failed: EROFS (Read-only file system)
/storage/emulated/0/Android/data/org.koitharu.kotatsu/cache/pages/fe06e192fa371e55918980f7a24c91ea.jpg: open failed: ENOENT (No such file or directory) /storage/emulated/0/Android/data/org.koitharu.kotatsu/cache/pages/fe06e192fa371e55918980f7a24c91ea.jpg: open failed: ENOENT (No such file or directory)
/storage/0000-0000/Android/data/org.koitharu.kotatsu/files/manga/e57d3af4-216e-48b2-8432-1541d58eea1e.tmp (I/O error) /storage/0000-0000/Android/data/org.koitharu.kotatsu/files/manga/e57d3af4-216e-48b2-8432-1541d58eea1e.tmp (I/O error)
*/ */
val groups = FNFE_MESSAGE_REGEX.matchEntire(message ?: return null)?.groupValues ?: return null val groups = FNFE_MESSAGE_REGEX.matchEntire(message ?: return null)?.groupValues ?: return null
val path = groups.getOrNull(1) val path = groups.getOrNull(1)
val error = groups.getOrNull(2) val error = groups.getOrNull(2)
val baseMessageIs = when (error) { val baseMessageIs = when (error) {
"EROFS" -> R.string.no_write_permission_to_file "EROFS" -> R.string.no_write_permission_to_file
"ENOENT" -> R.string.file_not_found "ENOENT" -> R.string.file_not_found
else -> return null else -> return null
} }
return if (path.isNullOrEmpty()) { return if (path.isNullOrEmpty()) {
resources.getString(baseMessageIs) resources.getString(baseMessageIs)
} else { } else {
resources.getString( resources.getString(
R.string.inline_preference_pattern, R.string.inline_preference_pattern,
resources.getString(baseMessageIs), resources.getString(baseMessageIs),
path, path,
) )
} }
} }

@ -7,111 +7,115 @@ import org.koitharu.kotatsu.core.ui.model.MangaOverride
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
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.MangaState
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
import org.koitharu.kotatsu.parsers.util.nullIfEmpty import org.koitharu.kotatsu.parsers.util.nullIfEmpty
import org.koitharu.kotatsu.reader.data.filterChapters import org.koitharu.kotatsu.reader.data.filterChapters
import java.util.Locale import java.util.Locale
data class MangaDetails( data class MangaDetails(
private val manga: Manga, private val manga: Manga,
private val localManga: LocalManga?, private val localManga: LocalManga?,
private val override: MangaOverride?, private val override: MangaOverride?,
val description: CharSequence?, val description: CharSequence?,
val isLoaded: Boolean, val isLoaded: Boolean,
) { ) {
constructor(manga: Manga) : this( constructor(manga: Manga) : this(
manga = manga, manga = manga,
localManga = null, localManga = null,
override = null, override = null,
description = null, description = null,
isLoaded = false, isLoaded = false,
) )
val id: Long val id: Long
get() = manga.id get() = manga.id
val allChapters: List<MangaChapter> by lazy { mergeChapters() } val allChapters: List<MangaChapter> by lazy { mergeChapters() }
val chapters: Map<String?, List<MangaChapter>> by lazy { val chapters: Map<String?, List<MangaChapter>> by lazy {
allChapters.groupBy { it.branch } allChapters.groupBy { it.branch }
} }
val isLocal val isLocal
get() = manga.isLocal get() = manga.isLocal
val local: LocalManga? val local: LocalManga?
get() = localManga ?: if (manga.isLocal) LocalManga(manga) else null get() = localManga ?: if (manga.isLocal) LocalManga(manga) else null
val coverUrl: String? val coverUrl: String?
get() = override?.coverUrl get() = override?.coverUrl
.ifNullOrEmpty { manga.largeCoverUrl } .ifNullOrEmpty { manga.largeCoverUrl }
.ifNullOrEmpty { manga.coverUrl } .ifNullOrEmpty { manga.coverUrl }
.ifNullOrEmpty { localManga?.manga?.coverUrl } .ifNullOrEmpty { localManga?.manga?.coverUrl }
?.nullIfEmpty() ?.nullIfEmpty()
private val mergedManga by lazy { val isRestricted: Boolean
if (localManga == null) { get() = manga.state == MangaState.RESTRICTED
// fast path
manga.withOverride(override) private val mergedManga by lazy {
} else { if (localManga == null) {
manga.copy( // fast path
title = override?.title.ifNullOrEmpty { manga.title }, manga.withOverride(override)
coverUrl = override?.coverUrl.ifNullOrEmpty { manga.coverUrl }, } else {
largeCoverUrl = override?.coverUrl.ifNullOrEmpty { manga.largeCoverUrl }, manga.copy(
contentRating = override?.contentRating ?: manga.contentRating, title = override?.title.ifNullOrEmpty { manga.title },
chapters = allChapters, coverUrl = override?.coverUrl.ifNullOrEmpty { manga.coverUrl },
) largeCoverUrl = override?.coverUrl.ifNullOrEmpty { manga.largeCoverUrl },
} contentRating = override?.contentRating ?: manga.contentRating,
} chapters = allChapters,
)
fun toManga() = mergedManga }
}
fun getLocale(): Locale? {
findAppropriateLocale(chapters.keys.singleOrNull())?.let { fun toManga() = mergedManga
return it
} fun getLocale(): Locale? {
return manga.source.getLocale() findAppropriateLocale(chapters.keys.singleOrNull())?.let {
} return it
}
fun filterChapters(branch: String?) = copy( return manga.source.getLocale()
manga = manga.filterChapters(branch), }
localManga = localManga?.run {
copy(manga = manga.filterChapters(branch)) fun filterChapters(branch: String?) = copy(
}, manga = manga.filterChapters(branch),
) localManga = localManga?.run {
copy(manga = manga.filterChapters(branch))
private fun mergeChapters(): List<MangaChapter> { },
val chapters = manga.chapters )
val localChapters = local?.manga?.chapters.orEmpty()
if (chapters.isNullOrEmpty()) { private fun mergeChapters(): List<MangaChapter> {
return localChapters val chapters = manga.chapters
} val localChapters = local?.manga?.chapters.orEmpty()
val localMap = if (localChapters.isNotEmpty()) { if (chapters.isNullOrEmpty()) {
localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id } return localChapters
} else { }
null val localMap = if (localChapters.isNotEmpty()) {
} localChapters.associateByTo(LinkedHashMap(localChapters.size)) { it.id }
val result = ArrayList<MangaChapter>(chapters.size) } else {
for (chapter in chapters) { null
val local = localMap?.remove(chapter.id) }
result += local ?: chapter val result = ArrayList<MangaChapter>(chapters.size)
} for (chapter in chapters) {
if (!localMap.isNullOrEmpty()) { val local = localMap?.remove(chapter.id)
result.addAll(localMap.values) result += local ?: chapter
} }
return result if (!localMap.isNullOrEmpty()) {
} result.addAll(localMap.values)
}
private fun findAppropriateLocale(name: String?): Locale? { return result
if (name.isNullOrEmpty()) { }
return null
} private fun findAppropriateLocale(name: String?): Locale? {
return Locale.getAvailableLocales().find { lc -> if (name.isNullOrEmpty()) {
name.contains(lc.getDisplayName(lc), ignoreCase = true) || return null
name.contains(lc.getDisplayName(Locale.ENGLISH), ignoreCase = true) || }
name.contains(lc.getDisplayLanguage(lc), ignoreCase = true) || return Locale.getAvailableLocales().find { lc ->
name.contains(lc.getDisplayLanguage(Locale.ENGLISH), ignoreCase = true) name.contains(lc.getDisplayName(lc), ignoreCase = true) ||
} name.contains(lc.getDisplayName(Locale.ENGLISH), ignoreCase = true) ||
} name.contains(lc.getDisplayLanguage(lc), ignoreCase = true) ||
name.contains(lc.getDisplayLanguage(Locale.ENGLISH), ignoreCase = true)
}
}
} }

Loading…
Cancel
Save