Restore covers using interceptor
This commit is contained in:
@@ -1,28 +1,28 @@
|
|||||||
package org.koitharu.kotatsu.browser.cloudflare
|
package org.koitharu.kotatsu.browser.cloudflare
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.core.app.NotificationChannelCompat
|
import androidx.core.app.NotificationChannelCompat
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.PendingIntentCompat
|
import androidx.core.app.PendingIntentCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import coil.EventListener
|
||||||
import coil.request.ErrorResult
|
import coil.request.ErrorResult
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
|
||||||
class CaptchaNotifier(
|
class CaptchaNotifier(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) : ImageRequest.Listener {
|
) : EventListener {
|
||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
|
||||||
fun notify(exception: CloudFlareProtectedException) {
|
fun notify(exception: CloudFlareProtectedException) {
|
||||||
val manager = NotificationManagerCompat.from(context)
|
if (!context.checkNotificationPermission()) {
|
||||||
if (!manager.areNotificationsEnabled()) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val manager = NotificationManagerCompat.from(context)
|
||||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||||
.setName(context.getString(R.string.captcha_required))
|
.setName(context.getString(R.string.captcha_required))
|
||||||
.setShowBadge(true)
|
.setShowBadge(true)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import coil.decode.SvgDecoder
|
|||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
import coil.util.DebugLogger
|
import coil.util.DebugLogger
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Lazy
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
@@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.SharedFlow
|
|||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
||||||
import org.koitharu.kotatsu.core.cache.ContentCache
|
import org.koitharu.kotatsu.core.cache.ContentCache
|
||||||
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
import org.koitharu.kotatsu.core.cache.MemoryContentCache
|
||||||
import org.koitharu.kotatsu.core.cache.StubContentCache
|
import org.koitharu.kotatsu.core.cache.StubContentCache
|
||||||
@@ -47,7 +47,7 @@ import org.koitharu.kotatsu.local.data.CacheDir
|
|||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.main.domain.CoverRestorer
|
import org.koitharu.kotatsu.main.domain.CoverRestoreInterceptor
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
import org.koitharu.kotatsu.reader.ui.thumbnails.MangaPageFetcher
|
||||||
@@ -91,7 +91,7 @@ interface AppModule {
|
|||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
imageProxyInterceptor: ImageProxyInterceptor,
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
coverRestorerProvider: Lazy<CoverRestorer>,
|
coverRestoreInterceptor: CoverRestoreInterceptor,
|
||||||
): ImageLoader {
|
): ImageLoader {
|
||||||
val diskCacheFactory = {
|
val diskCacheFactory = {
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||||
@@ -108,7 +108,7 @@ interface AppModule {
|
|||||||
.diskCache(diskCacheFactory)
|
.diskCache(diskCacheFactory)
|
||||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
.allowRgb565(context.isLowRamDevice())
|
.allowRgb565(context.isLowRamDevice())
|
||||||
.eventListenerFactory { coverRestorerProvider.get() }
|
.eventListener(CaptchaNotifier(context))
|
||||||
.components(
|
.components(
|
||||||
ComponentRegistry.Builder()
|
ComponentRegistry.Builder()
|
||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
@@ -116,6 +116,7 @@ interface AppModule {
|
|||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||||
.add(pageFetcherFactory)
|
.add(pageFetcherFactory)
|
||||||
.add(imageProxyInterceptor)
|
.add(imageProxyInterceptor)
|
||||||
|
.add(coverRestoreInterceptor)
|
||||||
.build(),
|
.build(),
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import coil.request.SuccessResult
|
|||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
import com.google.android.material.progressindicator.BaseProgressIndicator
|
import com.google.android.material.progressindicator.BaseProgressIndicator
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
|
|
||||||
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
|
import org.koitharu.kotatsu.core.ui.image.RegionBitmapDecoder
|
||||||
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
import org.koitharu.kotatsu.core.util.progress.ImageRequestIndicatorListener
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
@@ -29,7 +28,6 @@ fun ImageView.newImageRequest(lifecycleOwner: LifecycleOwner, data: Any?): Image
|
|||||||
.data(data)
|
.data(data)
|
||||||
.lifecycle(lifecycleOwner)
|
.lifecycle(lifecycleOwner)
|
||||||
.crossfade(context)
|
.crossfade(context)
|
||||||
.addListener(CaptchaNotifier(context.applicationContext))
|
|
||||||
.target(this)
|
.target(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +1,65 @@
|
|||||||
package org.koitharu.kotatsu.main.domain
|
package org.koitharu.kotatsu.main.domain
|
||||||
|
|
||||||
import androidx.collection.ArraySet
|
import androidx.collection.ArraySet
|
||||||
import androidx.lifecycle.coroutineScope
|
import coil.intercept.Interceptor
|
||||||
import coil.EventListener
|
|
||||||
import coil.ImageLoader
|
|
||||||
import coil.network.HttpException
|
import coil.network.HttpException
|
||||||
import coil.request.ErrorResult
|
import coil.request.ErrorResult
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageResult
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jsoup.HttpStatusException
|
import org.jsoup.HttpStatusException
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
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.core.util.ext.ifNullOrEmpty
|
||||||
import org.koitharu.kotatsu.parsers.exception.ParseException
|
import org.koitharu.kotatsu.parsers.exception.ParseException
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import java.util.Collections
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
|
||||||
|
|
||||||
class CoverRestorer @Inject constructor(
|
class CoverRestoreInterceptor @Inject constructor(
|
||||||
private val dataRepository: MangaDataRepository,
|
private val dataRepository: MangaDataRepository,
|
||||||
private val bookmarksRepository: BookmarksRepository,
|
private val bookmarksRepository: BookmarksRepository,
|
||||||
private val repositoryFactory: MangaRepository.Factory,
|
private val repositoryFactory: MangaRepository.Factory,
|
||||||
private val coilProvider: Provider<ImageLoader>,
|
) : Interceptor {
|
||||||
) : EventListener {
|
|
||||||
|
|
||||||
private val blacklist = ArraySet<String>()
|
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||||
|
|
||||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
super.onError(request, result)
|
val request = chain.request
|
||||||
if (!result.throwable.shouldRestore()) {
|
val result = chain.proceed(request)
|
||||||
return
|
if (result is ErrorResult && result.throwable.shouldRestore()) {
|
||||||
}
|
request.tags.tag<Manga>()?.let {
|
||||||
request.tags.tag<Manga>()?.let {
|
if (restoreManga(it)) {
|
||||||
restoreManga(it, request)
|
return chain.proceed(request.newBuilder().build())
|
||||||
}
|
} else {
|
||||||
request.tags.tag<Bookmark>()?.let {
|
return result
|
||||||
restoreBookmark(it, request)
|
}
|
||||||
}
|
}
|
||||||
}
|
request.tags.tag<Bookmark>()?.let {
|
||||||
|
if (restoreBookmark(it)) {
|
||||||
private fun restoreManga(manga: Manga, request: ImageRequest) {
|
return chain.proceed(request.newBuilder().build())
|
||||||
val key = manga.publicUrl
|
} else {
|
||||||
if (key in blacklist) {
|
return result
|
||||||
return
|
}
|
||||||
}
|
|
||||||
request.lifecycle.coroutineScope.launch {
|
|
||||||
val restored = runCatchingCancellable {
|
|
||||||
restoreMangaImpl(manga)
|
|
||||||
}.getOrDefault(false)
|
|
||||||
if (restored) {
|
|
||||||
request.newBuilder().enqueueWith(coilProvider.get())
|
|
||||||
} else {
|
|
||||||
blacklist.add(key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreManga(manga: Manga): Boolean {
|
||||||
|
val key = manga.publicUrl
|
||||||
|
if (!blacklist.add(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val restored = runCatchingCancellable {
|
||||||
|
restoreMangaImpl(manga)
|
||||||
|
}.getOrDefault(false)
|
||||||
|
if (restored) {
|
||||||
|
blacklist.remove(key)
|
||||||
|
}
|
||||||
|
return restored
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreMangaImpl(manga: Manga): Boolean {
|
private suspend fun restoreMangaImpl(manga: Manga): Boolean {
|
||||||
@@ -75,21 +76,18 @@ class CoverRestorer @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreBookmark(bookmark: Bookmark, request: ImageRequest) {
|
private suspend fun restoreBookmark(bookmark: Bookmark): Boolean {
|
||||||
val key = bookmark.imageUrl
|
val key = bookmark.imageUrl
|
||||||
if (key in blacklist) {
|
if (!blacklist.add(key)) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
request.lifecycle.coroutineScope.launch {
|
val restored = runCatchingCancellable {
|
||||||
val restored = runCatchingCancellable {
|
restoreBookmarkImpl(bookmark)
|
||||||
restoreBookmarkImpl(bookmark)
|
}.getOrDefault(false)
|
||||||
}.getOrDefault(false)
|
if (restored) {
|
||||||
if (restored) {
|
blacklist.remove(key)
|
||||||
request.newBuilder().enqueueWith(coilProvider.get())
|
|
||||||
} else {
|
|
||||||
blacklist.add(key)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return restored
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean {
|
private suspend fun restoreBookmarkImpl(bookmark: Bookmark): Boolean {
|
||||||
Reference in New Issue
Block a user