Compare commits

...

6 Commits
v4.3 ... v4.3.3

Author SHA1 Message Date
Koitharu
155af8889b Update version 2023-02-07 07:40:33 +02:00
Koitharu
61b7117b97 Allow to use own UserAgent for each manga source 2023-02-07 07:29:38 +02:00
Zakhar Timoshenko
0f4de329e5 Update parsers 2023-02-07 07:27:40 +02:00
Zakhar Timoshenko
9b290bea40 Change user agent to Chrome 2023-02-07 07:27:29 +02:00
Koitharu
7ffa15d2d7 Update parsers 2023-01-30 20:11:14 +02:00
Koitharu
e11e890818 Update parsers 2023-01-25 20:02:01 +02:00
42 changed files with 206 additions and 85 deletions

View File

@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
versionCode 511
versionName '4.3'
versionCode 514
versionName '4.3.3'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -84,7 +84,7 @@ afterEvaluate {
}
}
dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:cf00732023') {
implementation('com.github.KotatsuApp:kotatsu-parsers:00abaea324') {
exclude group: 'org.json', module: 'json'
}
@@ -106,7 +106,7 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
implementation 'com.google.android.material:material:1.8.0-rc01'
implementation 'com.google.android.material:material:1.8.0'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.5.1'

View File

@@ -26,7 +26,7 @@ fun bookmarkListAD(
binding.root.setOnLongClickListener(listener)
bind {
binding.imageViewThumb.newImageRequest(item.imageUrl)?.run {
binding.imageViewThumb.newImageRequest(item.imageUrl, item.manga.source)?.run {
referer(item.manga.publicUrl)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)

View File

@@ -14,7 +14,11 @@ import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
fun bookmarksGroupAD(
coil: ImageLoader,
@@ -45,7 +49,7 @@ fun bookmarksGroupAD(
binding.recyclerView.addItemDecoration(spacingDecoration)
selectionController.attachToRecyclerView(item.manga, binding.recyclerView)
}
binding.imageViewCover.newImageRequest(item.manga.coverUrl)?.run {
binding.imageViewCover.newImageRequest(item.manga.coverUrl, item.manga.source)?.run {
referer(item.manga.publicUrl)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)

View File

@@ -31,7 +31,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
}
with(binding.webView.settings) {
javaScriptEnabled = true
userAgentString = UserAgentInterceptor.userAgent
userAgentString = UserAgentInterceptor.userAgentChrome
}
binding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)

View File

@@ -12,7 +12,9 @@ import androidx.core.view.isInvisible
import androidx.fragment.app.setFragmentResult
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import okhttp3.Headers
import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.network.UserAgentInterceptor
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
@@ -42,7 +44,7 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
cacheMode = WebSettings.LOAD_DEFAULT
domStorageEnabled = true
databaseEnabled = true
userAgentString = UserAgentInterceptor.userAgent
userAgentString = arguments?.getString(ARG_UA) ?: UserAgentInterceptor.userAgentChrome
}
binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url.orEmpty())
CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
@@ -92,9 +94,13 @@ class CloudFlareDialog : AlertDialogFragment<FragmentCloudflareBinding>(), Cloud
const val TAG = "CloudFlareDialog"
const val EXTRA_RESULT = "result"
private const val ARG_URL = "url"
private const val ARG_UA = "ua"
fun newInstance(url: String) = CloudFlareDialog().withArgs(1) {
fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) {
putString(ARG_URL, url)
headers?.get(CommonHeaders.USER_AGENT)?.let {
putString(ARG_UA, it)
}
}
}
}

View File

@@ -85,6 +85,7 @@ interface AppModule {
@Singleton
fun provideOkHttpClient(
localStorageManager: LocalStorageManager,
userAgentInterceptor: UserAgentInterceptor,
cookieJar: CookieJar,
settings: AppSettings,
): OkHttpClient {
@@ -97,7 +98,7 @@ interface AppModule {
dns(DoHManager(cache, settings))
cache(cache)
addInterceptor(GZipInterceptor())
addInterceptor(UserAgentInterceptor())
addInterceptor(userAgentInterceptor)
addInterceptor(CloudFlareInterceptor())
}.build()
}

View File

@@ -1,7 +1,9 @@
package org.koitharu.kotatsu.core.exceptions
import okhttp3.Headers
import okio.IOException
class CloudFlareProtectedException(
val url: String
) : IOException("Protected by CloudFlare")
val url: String,
val headers: Headers,
) : IOException("Protected by CloudFlare")

View File

@@ -7,6 +7,7 @@ import androidx.collection.ArrayMap
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Headers
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
@@ -43,7 +44,7 @@ class ExceptionResolver private constructor(
}
suspend fun resolve(e: Throwable): Boolean = when (e) {
is CloudFlareProtectedException -> resolveCF(e.url)
is CloudFlareProtectedException -> resolveCF(e.url, e.headers)
is AuthRequiredException -> resolveAuthException(e.source)
is NotFoundException -> {
openInBrowser(e.url)
@@ -53,8 +54,8 @@ class ExceptionResolver private constructor(
else -> false
}
private suspend fun resolveCF(url: String): Boolean {
val dialog = CloudFlareDialog.newInstance(url)
private suspend fun resolveCF(url: String, headers: Headers): Boolean {
val dialog = CloudFlareDialog.newInstance(url, headers)
val fm = getFragmentManager()
return suspendCancellableCoroutine { cont ->
fm.clearFragmentResult(CloudFlareDialog.TAG)

View File

@@ -13,13 +13,17 @@ private const val SERVER_CLOUDFLARE = "cloudflare"
class CloudFlareInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
val request = chain.request()
val response = chain.proceed(request)
if (response.code == HTTP_FORBIDDEN || response.code == HTTP_UNAVAILABLE) {
if (response.header(HEADER_SERVER)?.startsWith(SERVER_CLOUDFLARE) == true) {
response.closeQuietly()
throw CloudFlareProtectedException(response.request.url.toString())
throw CloudFlareProtectedException(
url = response.request.url.toString(),
headers = request.headers,
)
}
}
return response
}
}
}

View File

@@ -1,24 +1,40 @@
package org.koitharu.kotatsu.core.network
import android.os.Build
import java.util.*
import dagger.Lazy
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
class UserAgentInterceptor : Interceptor {
@Singleton
class UserAgentInterceptor @Inject constructor(
private val mangaRepositoryFactoryLazy: Lazy<MangaRepository.Factory>,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
return chain.proceed(
if (request.header(CommonHeaders.USER_AGENT) == null) {
request.newBuilder()
.addHeader(CommonHeaders.USER_AGENT, userAgent)
.addHeader(CommonHeaders.USER_AGENT, getUserAgent(request))
.build()
} else request
} else request,
)
}
private fun getUserAgent(request: Request): String {
val source = request.tag(MangaSource::class.java) ?: return userAgent
val repository = mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository
return repository?.userAgent ?: userAgent
}
companion object {
val userAgent
@@ -28,16 +44,16 @@ class UserAgentInterceptor : Interceptor {
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language
)
Locale.getDefault().language,
) // TODO Decide what to do with this afterwards
val userAgentChrome
get() = (
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/100.0.4896.127 Mobile Safari/537.36"
).format(
Build.VERSION.RELEASE,
Build.MODEL,
)
Build.VERSION.RELEASE,
Build.MODEL,
)
}
}
}

View File

@@ -118,6 +118,7 @@ class ShortcutsUpdater @Inject constructor(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.size(iconSize.width, iconSize.height)
.tag(manga.source)
.precision(Precision.EXACT)
.scale(Scale.FILL)
.build(),

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.SafeDeferred
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.prefs.SourceSettings
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
@@ -39,6 +40,9 @@ class RemoteMangaRepository(
getConfig().defaultSortOrder = value
}
val userAgent: String?
get() = parser.headers?.get(CommonHeaders.USER_AGENT)
override suspend fun getList(offset: Int, query: String): List<Manga> {
return parser.getList(offset, query)
}

View File

@@ -53,7 +53,7 @@ class FaviconFetcher(
options.size.height.pxOrElse { FALLBACK_SIZE },
)
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
val response = loadIcon(icon.url, favicons.referer)
val response = loadIcon(icon.url, repo.userAgent, favicons.referer)
val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource()
return SourceResult(
@@ -63,11 +63,14 @@ class FaviconFetcher(
)
}
private suspend fun loadIcon(url: String, referer: String): Response {
private suspend fun loadIcon(url: String, userAgent: String?, referer: String): Response {
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, referer)
if (userAgent != null) {
request.header(CommonHeaders.USER_AGENT, userAgent)
}
@Suppress("UNCHECKED_CAST")
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
val response = okHttpClient.newCall(request.build()).await()

View File

@@ -24,6 +24,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
override fun <T> get(key: ConfigKey<T>): T {
return when (key) {
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
} as T
}
}
}

View File

@@ -254,7 +254,11 @@ class DetailsFragment :
R.id.imageView_cover -> {
startActivity(
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }),
ImageActivity.newIntent(
v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
manga.source,
),
scaleUpActivityOptionsOf(v).toBundle(),
)
}
@@ -337,6 +341,7 @@ class DetailsFragment :
.target(binding.imageViewCover)
.size(CoverSizeResolver(binding.imageViewCover))
.data(imageUrl)
.tag(manga.source)
.crossfade(context)
.referer(manga.publicUrl)
.lifecycle(viewLifecycleOwner)

View File

@@ -23,7 +23,7 @@ fun scrobblingInfoAD(
}
bind {
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
binding.imageViewCover.newImageRequest(item.coverUrl /* TODO */, null)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)

View File

@@ -100,7 +100,7 @@ class ScrobblingInfoBottomSheet :
R.id.imageView_cover -> {
val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return
val options = scaleUpActivityOptionsOf(v)
startActivity(ImageActivity.newIntent(v.context, coverUrl), options.toBundle())
startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
}
}
}
@@ -115,15 +115,13 @@ class ScrobblingInfoBottomSheet :
binding.ratingBar.rating = scrobbling.rating * binding.ratingBar.numStars
binding.textViewDescription.text = scrobbling.description
binding.spinnerStatus.setSelection(scrobbling.status?.ordinal ?: -1)
ImageRequest.Builder(context ?: return)
.target(binding.imageViewCover)
.data(scrobbling.coverUrl)
.crossfade(context)
.lifecycle(viewLifecycleOwner)
.placeholder(R.drawable.ic_placeholder)
.fallback(R.drawable.ic_placeholder)
.error(R.drawable.ic_error_placeholder)
.enqueueWith(coil)
binding.imageViewCover.newImageRequest(scrobbling.coverUrl)?.apply {
lifecycle(viewLifecycleOwner)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)
enqueueWith(coil)
}
}
override fun onMenuItemClick(item: MenuItem): Boolean {

View File

@@ -118,7 +118,7 @@ class DownloadManager @AssistedInject constructor(
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = CbzMangaOutput.get(destination, data)
val coverUrl = data.largeCoverUrl ?: data.coverUrl
downloadFile(coverUrl, data.publicUrl, destination, tempFileName).let { file ->
downloadFile(coverUrl, data.publicUrl, destination, tempFileName, repo.source).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
}
val chapters = checkNotNull(
@@ -139,7 +139,8 @@ class DownloadManager @AssistedInject constructor(
for ((pageIndex, page) in pages.withIndex()) {
runFailsafe(outState, pausingHandle) {
val url = repo.getPageUrl(page)
val file = cache[url] ?: downloadFile(url, page.referer, destination, tempFileName)
val file = cache[url]
?: downloadFile(url, page.referer, destination, tempFileName, repo.source)
output.addPage(
chapter = chapter,
file = file,
@@ -209,10 +210,17 @@ class DownloadManager @AssistedInject constructor(
}
}
private suspend fun downloadFile(url: String, referer: String, destination: File, tempFileName: String): File {
private suspend fun downloadFile(
url: String,
referer: String,
destination: File,
tempFileName: String,
source: MangaSource,
): File {
val request = Request.Builder()
.url(url)
.header(CommonHeaders.REFERER, referer)
.tag(source)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.get()
.build()
@@ -243,6 +251,7 @@ class DownloadManager @AssistedInject constructor(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.referer(manga.publicUrl)
.tag(manga.source)
.size(coverWidth, coverHeight)
.scale(Scale.FILL)
.build(),

View File

@@ -13,7 +13,11 @@ import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.onFirst
import org.koitharu.kotatsu.utils.ext.referer
fun downloadItemAD(
scope: CoroutineScope,
@@ -40,7 +44,7 @@ fun downloadItemAD(
bind {
job?.cancel()
job = item.progressAsFlow().onFirst { state ->
binding.imageViewCover.newImageRequest(state.manga.coverUrl)?.run {
binding.imageViewCover.newImageRequest(state.manga.coverUrl, state.manga.source)?.run {
referer(state.manga.publicUrl)
placeholder(state.cover)
fallback(R.drawable.ic_placeholder)
@@ -60,6 +64,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
}
is DownloadState.Done -> {
binding.textViewStatus.setText(R.string.download_complete)
binding.progressBar.isIndeterminate = false
@@ -69,6 +74,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
}
is DownloadState.Error -> {
binding.textViewStatus.setText(R.string.error_occurred)
binding.progressBar.isIndeterminate = false
@@ -79,6 +85,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = state.canRetry
binding.buttonResume.isVisible = state.canRetry
}
is DownloadState.PostProcessing -> {
binding.textViewStatus.setText(R.string.processing_)
binding.progressBar.isIndeterminate = true
@@ -88,6 +95,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
}
is DownloadState.Preparing -> {
binding.textViewStatus.setText(R.string.preparing_)
binding.progressBar.isIndeterminate = true
@@ -97,6 +105,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
}
is DownloadState.Progress -> {
binding.textViewStatus.setText(R.string.manga_downloading_)
binding.progressBar.isIndeterminate = false
@@ -109,6 +118,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
}
is DownloadState.Queued -> {
binding.textViewStatus.setText(R.string.queued)
binding.progressBar.isIndeterminate = false

View File

@@ -5,7 +5,9 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent
import android.view.View
import android.view.View.*
import android.view.View.OnClickListener
import android.view.View.OnLongClickListener
import android.view.View.OnTouchListener
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat
@@ -16,7 +18,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.animatorDurationScale
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getThemeColor
import org.koitharu.kotatsu.utils.ext.newImageRequest
fun categoryAD(
coil: ImageLoader,

View File

@@ -17,11 +17,12 @@ import coil.target.ViewTarget
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.databinding.ActivityImageBinding
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.indicator
import javax.inject.Inject
@AndroidEntryPoint
class ImageActivity : BaseActivity<ActivityImageBinding>() {
@@ -56,6 +57,7 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
.data(url)
.memoryCachePolicy(CachePolicy.DISABLED)
.lifecycle(this)
.tag(intent.getSerializableExtra(EXTRA_SOURCE) as? MangaSource)
.target(SsivTarget(binding.ssiv))
.indicator(binding.progressBar)
.enqueueWith(coil)
@@ -88,9 +90,12 @@ class ImageActivity : BaseActivity<ActivityImageBinding>() {
companion object {
fun newIntent(context: Context, url: String): Intent {
private const val EXTRA_SOURCE = "source"
fun newIntent(context: Context, url: String, source: MangaSource?): Intent {
return Intent(context, ImageActivity::class.java)
.setData(Uri.parse(url))
.putExtra(EXTRA_SOURCE, source)
}
}
}

View File

@@ -177,7 +177,7 @@ abstract class MangaListFragment :
private fun onError(e: Throwable) {
if (e is CloudFlareProtectedException) {
CloudFlareDialog.newInstance(e.url).show(childFragmentManager, CloudFlareDialog.TAG)
CloudFlareDialog.newInstance(e.url, e.headers).show(childFragmentManager, CloudFlareDialog.TAG)
} else {
Snackbar.make(
binding.recyclerView,

View File

@@ -39,7 +39,7 @@ fun mangaGridItemAD(
bind { payloads ->
binding.textViewTitle.text = item.title
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
binding.imageViewCover.newImageRequest(item.coverUrl, item.source)?.run {
referer(item.manga.publicUrl)
size(CoverSizeResolver(binding.imageViewCover))
placeholder(R.drawable.ic_placeholder)

View File

@@ -52,7 +52,7 @@ fun mangaListDetailedItemAD(
binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads)
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
binding.imageViewCover.newImageRequest(item.coverUrl, item.source)?.run {
referer(item.manga.publicUrl)
size(CoverSizeResolver(binding.imageViewCover))
placeholder(R.drawable.ic_placeholder)

View File

@@ -10,7 +10,11 @@ import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.textAndVisible
fun mangaListItemAD(
coil: ImageLoader,
@@ -31,7 +35,7 @@ fun mangaListItemAD(
bind {
binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
binding.imageViewCover.newImageRequest(item.coverUrl, item.source)?.run {
referer(item.manga.publicUrl)
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.list.ui.model
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
sealed interface MangaItemModel : ListModel {
@@ -10,4 +11,7 @@ sealed interface MangaItemModel : ListModel {
val coverUrl: String
val counter: Int
val progress: Float
}
val source: MangaSource
get() = manga.source
}

View File

@@ -99,7 +99,11 @@ class ImportService : CoroutineIntentService() {
if (manga != null) {
notification.setLargeIcon(
coil.execute(
ImageRequest.Builder(applicationContext).data(manga.coverUrl).referer(manga.publicUrl).build(),
ImageRequest.Builder(applicationContext)
.data(manga.coverUrl)
.tag(manga.source)
.referer(manga.publicUrl)
.build(),
).toBitmapOrNull(),
)
notification.setSubText(manga.title)

View File

@@ -191,6 +191,7 @@ class PageLoader @Inject constructor(
.header(CommonHeaders.REFERER, page.referer)
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.tag(page.source)
.build()
okHttp.newCall(request).await().use { response ->
check(response.isSuccessful) {

View File

@@ -13,11 +13,9 @@ import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.ViewSizeResolver
import com.google.android.material.R as materialR
import com.google.android.material.slider.LabelFormatter
import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
@@ -27,7 +25,14 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.assistedViewModels
import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.setValueRounded
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class ColorFilterConfigActivity :
@@ -115,6 +120,7 @@ class ColorFilterConfigActivity :
.referer(preview.referer)
.scale(Scale.FILL)
.decodeRegion()
.tag(preview.source)
.error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(binding.imageViewBefore))
.allowRgb565(false)

View File

@@ -5,9 +5,12 @@ import coil.ImageLoader
import coil.request.ImageRequest
import coil.size.Scale
import coil.size.Size
import com.google.android.material.R as materialR
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
@@ -19,6 +22,7 @@ import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.setTextColorAttr
import com.google.android.material.R as materialR
fun pageThumbnailAD(
coil: ImageLoader,
@@ -41,6 +45,7 @@ fun pageThumbnailAD(
ImageRequest.Builder(context)
.data(url)
.referer(item.page.referer)
.tag(item.page.source)
.size(thumbSize)
.scale(Scale.FILL)
.allowRgb565(true)

View File

@@ -17,7 +17,7 @@ fun searchSuggestionSourceAD(
lifecycleOwner: LifecycleOwner,
listener: SearchSuggestionListener,
) = adapterDelegateViewBinding<SearchSuggestionItem.Source, SearchSuggestionItem, ItemSearchSuggestionSourceBinding>(
{ inflater, parent -> ItemSearchSuggestionSourceBinding.inflate(inflater, parent, false) }
{ inflater, parent -> ItemSearchSuggestionSourceBinding.inflate(inflater, parent, false) },
) {
binding.switchLocal.setOnCheckedChangeListener { _, isChecked ->
@@ -31,7 +31,7 @@ fun searchSuggestionSourceAD(
binding.textViewTitle.text = item.source.title
binding.switchLocal.isChecked = item.isEnabled
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
binding.imageViewCover.newImageRequest(item.source.faviconUri())?.run {
binding.imageViewCover.newImageRequest(item.source.faviconUri(), item.source)?.run {
fallback(fallbackIcon)
placeholder(fallbackIcon)
error(fallbackIcon)
@@ -43,4 +43,4 @@ fun searchSuggestionSourceAD(
onViewRecycled {
binding.imageViewCover.disposeImageRequest()
}
}
}

View File

@@ -55,7 +55,7 @@ private fun searchSuggestionMangaGridAD(
}
bind {
binding.imageViewCover.newImageRequest(item.coverUrl)?.run {
binding.imageViewCover.newImageRequest(item.coverUrl, item.source)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)

View File

@@ -4,6 +4,7 @@ import android.view.inputmethod.EditorInfo
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.config.ConfigKey
@@ -29,15 +30,22 @@ fun PreferenceFragmentCompat.addPreferencesFromRepository(repository: RemoteMang
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI,
hint = key.defaultValue,
validator = DomainValidator(),
)
),
)
setTitle(R.string.domain)
setDialogTitle(R.string.domain)
}
}
is ConfigKey.ShowSuspiciousContent -> {
SwitchPreferenceCompat(requireContext()).apply {
setDefaultValue(key.defaultValue)
setTitle(R.string.show_suspicious_content)
}
}
}
preference.isIconSpaceReserved = false
preference.key = key.key
screen.addPreference(preference)
}
}
}

View File

@@ -105,6 +105,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
}
private fun resolveError(error: Throwable) {
view ?: return
viewLifecycleScope.launch {
if (exceptionResolver.resolve(error)) {
val pref = findPreference<Preference>(KEY_AUTH) ?: return@launch

View File

@@ -20,7 +20,7 @@ import org.koitharu.kotatsu.utils.image.FaviconFallbackDrawable
fun sourceConfigHeaderDelegate() =
adapterDelegateViewBinding<SourceConfigItem.Header, SourceConfigItem, ItemFilterHeaderBinding>(
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) }
{ layoutInflater, parent -> ItemFilterHeaderBinding.inflate(layoutInflater, parent, false) },
) {
bind {
@@ -31,7 +31,7 @@ fun sourceConfigHeaderDelegate() =
fun sourceConfigGroupDelegate(
listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.LocaleGroup, SourceConfigItem, ItemExpandableBinding>(
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) }
{ layoutInflater, parent -> ItemExpandableBinding.inflate(layoutInflater, parent, false) },
) {
binding.root.setOnClickListener {
@@ -50,7 +50,7 @@ fun sourceConfigItemDelegate(
lifecycleOwner: LifecycleOwner,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigBinding>(
{ layoutInflater, parent -> ItemSourceConfigBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable }
on = { item, _, _ -> item is SourceConfigItem.SourceItem && !item.isDraggable },
) {
binding.switchToggle.setOnCheckedChangeListener { _, isChecked ->
@@ -62,7 +62,7 @@ fun sourceConfigItemDelegate(
binding.switchToggle.isChecked = item.isEnabled
binding.textViewDescription.textAndVisible = item.summary
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
binding.imageViewIcon.newImageRequest(item.source.faviconUri())?.run {
binding.imageViewIcon.newImageRequest(item.source.faviconUri(), item.source)?.run {
crossfade(context)
error(fallbackIcon)
placeholder(fallbackIcon)
@@ -82,7 +82,7 @@ fun sourceConfigDraggableItemDelegate(
listener: SourceConfigListener,
) = adapterDelegateViewBinding<SourceConfigItem.SourceItem, SourceConfigItem, ItemSourceConfigDraggableBinding>(
{ layoutInflater, parent -> ItemSourceConfigDraggableBinding.inflate(layoutInflater, parent, false) },
on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable }
on = { item, _, _ -> item is SourceConfigItem.SourceItem && item.isDraggable },
) {
val eventListener = object :
@@ -117,5 +117,5 @@ fun sourceConfigDraggableItemDelegate(
}
fun sourceConfigEmptySearchDelegate() = adapterDelegate<SourceConfigItem.EmptySearchResult, SourceConfigItem>(
R.layout.item_sources_empty
) { }
R.layout.item_sources_empty,
) { }

View File

@@ -11,9 +11,7 @@ import androidx.activity.result.contract.ActivityResultContract
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import com.google.android.material.R as materialR
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserCallback
@@ -26,6 +24,8 @@ import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import org.koitharu.kotatsu.parsers.MangaParserAuthProvider
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.TaggedActivityResult
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint
class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
@@ -44,7 +44,8 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
finishAfterTransition()
return
}
authProvider = (mangaRepositoryFactory.create(source) as? RemoteMangaRepository)?.getAuthProvider() ?: run {
val repository = mangaRepositoryFactory.create(source) as? RemoteMangaRepository
authProvider = (repository)?.getAuthProvider() ?: run {
Toast.makeText(
this,
getString(R.string.auth_not_supported_by, source.title),
@@ -59,7 +60,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
}
with(binding.webView.settings) {
javaScriptEnabled = true
userAgentString = UserAgentInterceptor.userAgentChrome
userAgentString = repository.userAgent ?: UserAgentInterceptor.userAgent
}
binding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
@@ -96,6 +97,7 @@ class SourceAuthActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallba
finishAfterTransition()
true
}
else -> super.onOptionsItemSelected(item)
}

View File

@@ -28,7 +28,7 @@ fun feedItemAD(
bind {
binding.textViewTitle.isBold = item.isNew
binding.textViewSummary.isBold = item.isNew
binding.imageViewCover.newImageRequest(item.imageUrl)?.run {
binding.imageViewCover.newImageRequest(item.imageUrl, item.manga.source)?.run {
placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder)

View File

@@ -155,7 +155,11 @@ class TrackWorker @AssistedInject constructor(
setNumber(newChapters.size)
setLargeIcon(
coil.execute(
ImageRequest.Builder(applicationContext).data(manga.coverUrl).referer(manga.publicUrl).build(),
ImageRequest.Builder(applicationContext)
.data(manga.coverUrl)
.referer(manga.publicUrl)
.tag(manga.source)
.build(),
).toBitmapOrNull(),
)
setSmallIcon(R.drawable.ic_stat_book_plus)

View File

@@ -13,16 +13,18 @@ import com.google.android.material.progressindicator.BaseProgressIndicator
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.image.RegionBitmapDecoder
import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener
fun ImageView.newImageRequest(url: Any?): ImageRequest.Builder? {
fun ImageView.newImageRequest(url: Any?, mangaSource: MangaSource? = null): ImageRequest.Builder? {
val current = CoilUtils.result(this)
if (current != null && current.request.data == url) {
return null
}
return ImageRequest.Builder(context)
.data(url)
.tag(mangaSource)
.crossfade(context)
.target(this)
}
@@ -45,6 +47,7 @@ fun ImageResult.toBitmapOrNull() = when (this) {
} catch (_: Throwable) {
null
}
is ErrorResult -> null
}

View File

@@ -53,6 +53,7 @@ class RecentListFactory(
ImageRequest.Builder(context)
.data(item.coverUrl)
.size(coverSize)
.tag(item.source)
.transformations(transformation)
.build(),
).requireBitmap()

View File

@@ -64,6 +64,7 @@ class ShelfListFactory(
ImageRequest.Builder(context)
.data(item.coverUrl)
.size(coverSize)
.tag(item.source)
.transformations(transformation)
.build(),
).requireBitmap()

View File

@@ -405,4 +405,5 @@
<string name="share_logs">Share logs</string>
<string name="enable_logging">Enable logging</string>
<string name="enable_logging_summary">Record some actions for debug purposes</string>
<string name="show_suspicious_content">Show suspicious content</string>
</resources>