diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt index a8379c9aa..b170bb6ff 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/domain/BookmarksRepository.kt @@ -52,6 +52,13 @@ class BookmarksRepository @Inject constructor( } } + suspend fun updateBookmark(bookmark: Bookmark, imageUrl: String) { + val entity = bookmark.toEntity().copy( + imageUrl = imageUrl, + ) + db.bookmarksDao.upsert(listOf(entity)) + } + suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) { check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) { "Bookmark not found" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt index f8b9c411e..e5ffa861c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/adapter/BookmarkListAD.kt @@ -34,6 +34,7 @@ fun bookmarkListAD( fallback(R.drawable.ic_placeholder) error(R.drawable.ic_error_placeholder) allowRgb565(true) + tag(item) decodeRegion(item.scroll) source(item.manga.source) enqueueWith(coil) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarkLargeAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarkLargeAD.kt index 6c19f1900..d6ba86a61 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarkLargeAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/bookmarks/ui/sheet/BookmarkLargeAD.kt @@ -35,6 +35,7 @@ fun bookmarkLargeAD( fallback(R.drawable.ic_placeholder) error(R.drawable.ic_error_placeholder) allowRgb565(true) + tag(item) decodeRegion(item.scroll) source(item.manga.source) enqueueWith(coil) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt index 8dc1dc098..f44b8c440 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/AppModule.kt @@ -13,6 +13,7 @@ import coil.decode.SvgDecoder import coil.disk.DiskCache import coil.util.DebugLogger import dagger.Binds +import dagger.Lazy import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -46,6 +47,7 @@ import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.CbzFetcher import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.domain.model.LocalManga +import org.koitharu.kotatsu.main.domain.CoverRestorer import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher @@ -89,6 +91,7 @@ interface AppModule { mangaRepositoryFactory: MangaRepository.Factory, imageProxyInterceptor: ImageProxyInterceptor, pageFetcherFactory: MangaPageFetcher.Factory, + coverRestorerProvider: Lazy, ): ImageLoader { val diskCacheFactory = { val rootDir = context.externalCacheDir ?: context.cacheDir @@ -105,6 +108,7 @@ interface AppModule { .diskCache(diskCacheFactory) .logger(if (BuildConfig.DEBUG) DebugLogger() else null) .allowRgb565(context.isLowRamDevice()) + .eventListenerFactory { coverRestorerProvider.get() } .components( ComponentRegistry.Builder() .add(SvgDecoder.Factory()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index e52b7e011..dc934cf53 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -111,6 +111,11 @@ class RemoteMangaRepository( return details.await() } + suspend fun find(manga: Manga): Manga? { + val list = getList(0, manga.title) + return list.find { x -> x.id == manga.id } + } + fun getAuthProvider(): MangaParserAuthProvider? = parser as? MangaParserAuthProvider fun getConfigKeys(): List> = ArrayList>().also { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt index 782811b67..69735b012 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Coil.kt @@ -29,7 +29,7 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image .data(data) .lifecycle(lifecycleOwner) .crossfade(context) - .listener(CaptchaNotifier(context.applicationContext)) + .addListener(CaptchaNotifier(context.applicationContext)) .target(this) } @@ -65,11 +65,11 @@ fun ImageResult.toBitmapOrNull() = when (this) { } fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRequest.Builder { - return listener(ImageRequestIndicatorListener(listOf(indicator))) + return addListener(ImageRequestIndicatorListener(listOf(indicator))) } fun ImageRequest.Builder.indicator(indicators: List>): ImageRequest.Builder { - return listener(ImageRequestIndicatorListener(indicators)) + return addListener(ImageRequestIndicatorListener(indicators)) } fun ImageRequest.Builder.decodeRegion( @@ -86,3 +86,30 @@ fun ImageRequest.Builder.crossfade(context: Context): ImageRequest.Builder { fun ImageRequest.Builder.source(source: MangaSource?): ImageRequest.Builder { return tag(MangaSource::class.java, source) } + +fun ImageRequest.Builder.addListener(listener: ImageRequest.Listener): ImageRequest.Builder { + val existing = build().listener + return listener( + when (existing) { + null -> listener + is CompositeImageRequestListener -> existing + listener + else -> CompositeImageRequestListener(arrayOf(existing, listener)) + }, + ) +} + +private class CompositeImageRequestListener( + private val delegates: Array, +) : ImageRequest.Listener { + + override fun onCancel(request: ImageRequest) = delegates.forEach { it.onCancel(request) } + + override fun onError(request: ImageRequest, result: ErrorResult) = delegates.forEach { it.onError(request, result) } + + override fun onStart(request: ImageRequest) = delegates.forEach { it.onStart(request) } + + override fun onSuccess(request: ImageRequest, result: SuccessResult) = + delegates.forEach { it.onSuccess(request, result) } + + operator fun plus(other: ImageRequest.Listener) = CompositeImageRequestListener(delegates + other) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt index d86caff0c..16b24a8f3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaGridItemAD.kt @@ -48,6 +48,7 @@ fun mangaGridItemAD( error(R.drawable.ic_error_placeholder) transformations(TrimTransformation()) allowRgb565(true) + tag(item.manga) source(item.source) enqueueWith(coil) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt index 3506ef54c..dfbc7e660 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListDetailedItemAD.kt @@ -61,6 +61,7 @@ fun mangaListDetailedItemAD( error(R.drawable.ic_error_placeholder) transformations(TrimTransformation()) allowRgb565(true) + tag(item.manga) source(item.source) enqueueWith(coil) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt index 5d7f656dc..222db782a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/list/ui/adapter/MangaListItemAD.kt @@ -42,6 +42,7 @@ fun mangaListItemAD( error(R.drawable.ic_error_placeholder) allowRgb565(true) transformations(TrimTransformation()) + tag(item.manga) source(item.source) enqueueWith(coil) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestorer.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestorer.kt new file mode 100644 index 000000000..0471a1cdc --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/domain/CoverRestorer.kt @@ -0,0 +1,90 @@ +package org.koitharu.kotatsu.main.domain + +import androidx.lifecycle.coroutineScope +import coil.EventListener +import coil.ImageLoader +import coil.network.HttpException +import coil.request.ErrorResult +import coil.request.ImageRequest +import kotlinx.coroutines.launch +import org.koitharu.kotatsu.bookmarks.domain.Bookmark +import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository +import org.koitharu.kotatsu.core.parser.MangaDataRepository +import org.koitharu.kotatsu.core.parser.MangaRepository +import org.koitharu.kotatsu.core.parser.RemoteMangaRepository +import org.koitharu.kotatsu.core.util.ext.enqueueWith +import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import javax.inject.Inject +import javax.inject.Provider + +class CoverRestorer @Inject constructor( + private val dataRepository: MangaDataRepository, + private val bookmarksRepository: BookmarksRepository, + private val repositoryFactory: MangaRepository.Factory, + private val coilProvider: Provider, +) : EventListener { + + override fun onError(request: ImageRequest, result: ErrorResult) { + super.onError(request, result) + if (result.throwable !is HttpException) { + return + } + request.tags.tag()?.let { + restoreManga(it, request) + } + request.tags.tag()?.let { + restoreBookmark(it, request) + } + } + + private fun restoreManga(manga: Manga, request: ImageRequest) { + request.lifecycle.coroutineScope.launch { + val restored = runCatchingCancellable { + restoreMangaImpl(manga) + }.getOrDefault(false) + if (restored) { + request.newBuilder().enqueueWith(coilProvider.get()) + } + } + } + + private suspend fun restoreMangaImpl(manga: Manga): Boolean { + if (dataRepository.findMangaById(manga.id) == null) { + return false + } + val repo = repositoryFactory.create(manga.source) as? RemoteMangaRepository ?: return false + val fixed = repo.find(manga) ?: return false + return if (fixed != manga) { + dataRepository.storeManga(fixed) + fixed.coverUrl != manga.coverUrl + } else { + false + } + } + + private fun restoreBookmark(bookmark: Bookmark, request: ImageRequest) { + request.lifecycle.coroutineScope.launch { + val restored = runCatchingCancellable { + restoreBookmarkImpl(bookmark) + }.getOrDefault(false) + if (restored) { + request.newBuilder().enqueueWith(coilProvider.get()) + } + } + } + + private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean { + val repo = repositoryFactory.create(bookmark.manga.source) as? RemoteMangaRepository ?: return false + val chapter = repo.getDetails(bookmark.manga).chapters?.find { it.id == bookmark.chapterId } ?: return false + val page = repo.getPages(chapter)[bookmark.page] + val imageUrl = page.preview.ifNullOrEmpty { page.url } + return if (imageUrl != bookmark.imageUrl) { + bookmarksRepository.updateBookmark(bookmark, imageUrl) + true + } else { + false + } + } +}