Merge branch 'devel' into feature/mal

pull/302/head
Zakhar Timoshenko 3 years ago
commit 5c44a4dbb3

@ -16,7 +16,7 @@ Download APK directly from GitHub:
### Main Features ### Main Features
* Online manga catalogues * Online [manga catalogues](https://github.com/KotatsuApp/kotatsu-parsers)
* Search manga by name and genres * Search manga by name and genres
* Reading history and bookmarks * Reading history and bookmarks
* Favourites organized by user-defined categories * Favourites organized by user-defined categories
@ -24,7 +24,7 @@ Download APK directly from GitHub:
* Tablet-optimized Material You UI * Tablet-optimized Material You UI
* Standard and Webtoon-optimized reader * Standard and Webtoon-optimized reader
* Notifications about new chapters with updates feed * Notifications about new chapters with updates feed
* Shikimori integration (manga tracking) * Integration with manga tracking services: Shikimori, AniList, MAL (coming soon)
* Password/fingerprint protect access to the app * Password/fingerprint protect access to the app
* History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices * History and favourites [synchronization](https://github.com/KotatsuApp/kotatsu-syncserver) across devices

@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 513 versionCode 514
versionName '4.3.2' versionName '4.3.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -86,7 +86,7 @@ afterEvaluate {
} }
} }
dependencies { dependencies {
implementation('com.github.KotatsuApp:kotatsu-parsers:c28e2a72d5') { implementation('com.github.KotatsuApp:kotatsu-parsers:05d705ac03') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }

@ -1,15 +1,20 @@
package org.koitharu.kotatsu.core.parser package org.koitharu.kotatsu.core.parser
import java.util.*
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.config.ConfigKey import org.koitharu.kotatsu.parsers.config.ConfigKey
import org.koitharu.kotatsu.parsers.model.* 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.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.EnumSet
/** /**
* This parser is just for parser development, it should not be used in releases * This parser is just for parser development, it should not be used in releases
*/ */
class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) { class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("", null) get() = ConfigKey.Domain("", null)
@ -37,4 +42,4 @@ class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaS
override suspend fun getTags(): Set<MangaTag> { override suspend fun getTags(): Set<MangaTag> {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

@ -11,7 +11,6 @@ import org.koitharu.kotatsu.databinding.ItemBookmarkBinding
import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
fun bookmarkListAD( fun bookmarkListAD(
coil: ImageLoader, coil: ImageLoader,
@ -26,8 +25,7 @@ fun bookmarkListAD(
binding.root.setOnLongClickListener(listener) binding.root.setOnLongClickListener(listener)
bind { 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) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)

@ -14,7 +14,10 @@ import org.koitharu.kotatsu.bookmarks.ui.model.BookmarksGroup
import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding import org.koitharu.kotatsu.databinding.ItemBookmarksGroupBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga 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
fun bookmarksGroupAD( fun bookmarksGroupAD(
coil: ImageLoader, coil: ImageLoader,
@ -45,8 +48,7 @@ fun bookmarksGroupAD(
binding.recyclerView.addItemDecoration(spacingDecoration) binding.recyclerView.addItemDecoration(spacingDecoration)
selectionController.attachToRecyclerView(item.manga, binding.recyclerView) 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) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)

@ -8,16 +8,14 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.google.android.material.R as materialR
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.network.UserAgentInterceptor import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
import com.google.android.material.R as materialR
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback { class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback {
@ -31,7 +29,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
} }
with(binding.webView.settings) { with(binding.webView.settings) {
javaScriptEnabled = true javaScriptEnabled = true
userAgentString = UserAgentInterceptor.userAgentChrome userAgentString = CommonHeadersInterceptor.userAgentChrome
} }
binding.webView.webViewClient = BrowserClient(this) binding.webView.webViewClient = BrowserClient(this)
binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar) binding.webView.webChromeClient = ProgressChromeClient(binding.progressBar)
@ -72,6 +70,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
finishAfterTransition() finishAfterTransition()
true true
} }
R.id.action_browser -> { R.id.action_browser -> {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(binding.webView.url) intent.data = Uri.parse(binding.webView.url)
@ -81,6 +80,7 @@ class BrowserActivity : BaseActivity<ActivityBrowserBinding>(), BrowserCallback
} }
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }

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

@ -85,6 +85,7 @@ interface AppModule {
@Singleton @Singleton
fun provideOkHttpClient( fun provideOkHttpClient(
localStorageManager: LocalStorageManager, localStorageManager: LocalStorageManager,
commonHeadersInterceptor: CommonHeadersInterceptor,
cookieJar: CookieJar, cookieJar: CookieJar,
settings: AppSettings, settings: AppSettings,
): OkHttpClient { ): OkHttpClient {
@ -97,7 +98,7 @@ interface AppModule {
dns(DoHManager(cache, settings)) dns(DoHManager(cache, settings))
cache(cache) cache(cache)
addInterceptor(GZipInterceptor()) addInterceptor(GZipInterceptor())
addInterceptor(UserAgentInterceptor()) addInterceptor(commonHeadersInterceptor)
addInterceptor(CloudFlareInterceptor()) addInterceptor(CloudFlareInterceptor())
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
addInterceptor(CurlLoggingInterceptor()) addInterceptor(CurlLoggingInterceptor())

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

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

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

@ -0,0 +1,77 @@
package org.koitharu.kotatsu.core.network
import android.os.Build
import android.util.Log
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 org.koitharu.kotatsu.parsers.util.mergeWith
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CommonHeadersInterceptor @Inject constructor(
private val mangaRepositoryFactoryLazy: Lazy<MangaRepository.Factory>,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val source = request.tag(MangaSource::class.java)
val repository = if (source != null) {
mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository
} else {
if (BuildConfig.DEBUG) {
Log.w("Http", "Request without source tag: ${request.url}")
}
null
}
val headersBuilder = request.headers.newBuilder()
repository?.headers?.let {
headersBuilder.mergeWith(it, replaceExisting = false)
}
if (headersBuilder[CommonHeaders.USER_AGENT] == null) {
headersBuilder[CommonHeaders.USER_AGENT] = userAgentFallback
}
if (headersBuilder[CommonHeaders.REFERER] == null && repository != null) {
headersBuilder[CommonHeaders.REFERER] = "https://${repository.domain}/"
}
val newRequest = request.newBuilder().headers(headersBuilder.build()).build()
return repository?.intercept(ProxyChain(chain, newRequest)) ?: chain.proceed(newRequest)
}
private class ProxyChain(
private val delegate: Interceptor.Chain,
private val request: Request,
) : Interceptor.Chain by delegate {
override fun request(): Request = request
}
companion object {
val userAgentFallback
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
Locale.getDefault().language,
)
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,
)
}
}

@ -1,43 +0,0 @@
package org.koitharu.kotatsu.core.network
import android.os.Build
import java.util.*
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
class UserAgentInterceptor : 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, userAgentChrome)
.build()
} else request
)
}
companion object {
val userAgent
get() = "Kotatsu/%s (Android %s; %s; %s %s; %s)".format(
BuildConfig.VERSION_NAME,
Build.VERSION.RELEASE,
Build.MODEL,
Build.BRAND,
Build.DEVICE,
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,
)
}
}

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

@ -6,6 +6,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainCoroutineDispatcher import kotlinx.coroutines.MainCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.cache.ContentCache
import org.koitharu.kotatsu.core.cache.SafeDeferred import org.koitharu.kotatsu.core.cache.SafeDeferred
import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.core.prefs.SourceSettings
@ -19,13 +22,14 @@ 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.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.domain
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class RemoteMangaRepository( class RemoteMangaRepository(
private val parser: MangaParser, private val parser: MangaParser,
private val cache: ContentCache, private val cache: ContentCache,
) : MangaRepository { ) : MangaRepository, Interceptor {
override val source: MangaSource override val source: MangaSource
get() = parser.source get() = parser.source
@ -39,6 +43,20 @@ class RemoteMangaRepository(
getConfig().defaultSortOrder = value getConfig().defaultSortOrder = value
} }
val domain: String
get() = parser.domain
val headers: Headers?
get() = parser.headers
override fun intercept(chain: Interceptor.Chain): Response {
return if (parser is Interceptor) {
parser.intercept(chain)
} else {
chain.proceed(chain.request())
}
}
override suspend fun getList(offset: Int, query: String): List<Manga> { override suspend fun getList(offset: Int, query: String): List<Manga> {
return parser.getList(offset, query) return parser.getList(offset, query)
} }

@ -20,7 +20,6 @@ import okhttp3.Response
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.network.CommonHeaders
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.local.data.CacheDir import org.koitharu.kotatsu.local.data.CacheDir
@ -53,7 +52,7 @@ class FaviconFetcher(
options.size.height.pxOrElse { FALLBACK_SIZE }, options.size.height.pxOrElse { FALLBACK_SIZE },
) )
val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" } val icon = checkNotNull(favicons.find(sizePx)) { "No favicons found" }
val response = loadIcon(icon.url, favicons.referer) val response = loadIcon(icon.url, mangaSource)
val responseBody = response.requireBody() val responseBody = response.requireBody()
val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource() val source = writeToDiskCache(responseBody)?.toImageSource() ?: responseBody.toImageSource()
return SourceResult( return SourceResult(
@ -63,11 +62,11 @@ class FaviconFetcher(
) )
} }
private suspend fun loadIcon(url: String, referer: String): Response { private suspend fun loadIcon(url: String, source: MangaSource): Response {
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.get() .get()
.header(CommonHeaders.REFERER, referer) .tag(MangaSource::class.java, source)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) } options.tags.asMap().forEach { request.tag(it.key as Class<Any>, it.value) }
val response = okHttpClient.newCall(request.build()).await() val response = okHttpClient.newCall(request.build()).await()

@ -22,8 +22,6 @@ import org.koitharu.kotatsu.utils.ext.observe
import org.koitharu.kotatsu.utils.ext.putEnumValue import org.koitharu.kotatsu.utils.ext.putEnumValue
import org.koitharu.kotatsu.utils.ext.toUriOrNull import org.koitharu.kotatsu.utils.ext.toUriOrNull
import java.io.File import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Collections import java.util.Collections
import java.util.EnumSet import java.util.EnumSet
import java.util.Locale import java.util.Locale
@ -263,12 +261,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
return policy.isNetworkAllowed(connectivityManager) return policy.isNetworkAllowed(connectivityManager)
} }
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun getSuggestionsTagsBlacklistRegex(): Regex? { fun getSuggestionsTagsBlacklistRegex(): Regex? {
val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',') val string = prefs.getString(KEY_SUGGESTIONS_EXCLUDE_TAGS, null)?.trimEnd(' ', ',')
if (string.isNullOrEmpty()) { if (string.isNullOrEmpty()) {
@ -317,7 +309,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
const val KEY_THEME = "theme" const val KEY_THEME = "theme"
const val KEY_COLOR_THEME = "color_theme" const val KEY_COLOR_THEME = "color_theme"
const val KEY_THEME_AMOLED = "amoled_theme" const val KEY_THEME_AMOLED = "amoled_theme"
const val KEY_DATE_FORMAT = "date_format"
const val KEY_SOURCES_ORDER = "sources_order_2" const val KEY_SOURCES_ORDER = "sources_order_2"
const val KEY_SOURCES_HIDDEN = "sources_hidden" const val KEY_SOURCES_HIDDEN = "sources_hidden"
const val KEY_TRAFFIC_WARNING = "traffic_warning" const val KEY_TRAFFIC_WARNING = "traffic_warning"

@ -12,8 +12,11 @@ enum class ColorScheme(
DEFAULT(R.style.Theme_Kotatsu, R.string.system_default), DEFAULT(R.style.Theme_Kotatsu, R.string.system_default),
MONET(R.style.Theme_Kotatsu_Monet, R.string.theme_name_dynamic), MONET(R.style.Theme_Kotatsu_Monet, R.string.theme_name_dynamic),
MINT(R.style.Theme_Kotatsu_Mint, R.string.theme_name_mint), MIKU(R.style.Theme_Kotatsu_Miku, R.string.theme_name_miku),
OCTOBER(R.style.Theme_Kotatsu_October, R.string.theme_name_october), RENA(R.style.Theme_Kotatsu_Asuka, R.string.theme_name_asuka),
FROG(R.style.Theme_Kotatsu_Mion, R.string.theme_name_mion),
BLUEBERRY(R.style.Theme_Kotatsu_Rikka, R.string.theme_name_rikka),
NAME2(R.style.Theme_Kotatsu_Sakura, R.string.theme_name_sakura),
; ;
companion object { companion object {

@ -50,7 +50,6 @@ import org.koitharu.kotatsu.utils.ext.drawableTop
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty import org.koitharu.kotatsu.utils.ext.ifNullOrEmpty
import org.koitharu.kotatsu.utils.ext.measureHeight import org.koitharu.kotatsu.utils.ext.measureHeight
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.resolveDp import org.koitharu.kotatsu.utils.ext.resolveDp
import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf import org.koitharu.kotatsu.utils.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.utils.ext.textAndVisible import org.koitharu.kotatsu.utils.ext.textAndVisible
@ -254,7 +253,11 @@ class DetailsFragment :
R.id.imageView_cover -> { R.id.imageView_cover -> {
startActivity( startActivity(
ImageActivity.newIntent(v.context, manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl }), ImageActivity.newIntent(
v.context,
manga.largeCoverUrl.ifNullOrEmpty { manga.coverUrl },
manga.source,
),
scaleUpActivityOptionsOf(v).toBundle(), scaleUpActivityOptionsOf(v).toBundle(),
) )
} }
@ -337,8 +340,8 @@ class DetailsFragment :
.target(binding.imageViewCover) .target(binding.imageViewCover)
.size(CoverSizeResolver(binding.imageViewCover)) .size(CoverSizeResolver(binding.imageViewCover))
.data(imageUrl) .data(imageUrl)
.tag(manga.source)
.crossfade(context) .crossfade(context)
.referer(manga.publicUrl)
.lifecycle(viewLifecycleOwner) .lifecycle(viewLifecycleOwner)
.placeholderMemoryCacheKey(manga.coverUrl) .placeholderMemoryCacheKey(manga.coverUrl)
val previousDrawable = lastResult?.drawable val previousDrawable = lastResult?.drawable

@ -72,7 +72,6 @@ class DetailsViewModel @AssistedInject constructor(
private val delegate = MangaDetailsDelegate( private val delegate = MangaDetailsDelegate(
intent = intent, intent = intent,
settings = settings,
mangaDataRepository = mangaDataRepository, mangaDataRepository = mangaDataRepository,
historyRepository = historyRepository, historyRepository = historyRepository,
localMangaRepository = localMangaRepository, localMangaRepository = localMangaRepository,

@ -7,7 +7,6 @@ import org.koitharu.kotatsu.base.domain.MangaIntent
import org.koitharu.kotatsu.core.model.MangaHistory import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.core.model.getPreferredBranch import org.koitharu.kotatsu.core.model.getPreferredBranch
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.details.ui.model.ChapterListItem
import org.koitharu.kotatsu.details.ui.model.toListItem import org.koitharu.kotatsu.details.ui.model.toListItem
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
@ -21,7 +20,6 @@ import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class MangaDetailsDelegate( class MangaDetailsDelegate(
private val intent: MangaIntent, private val intent: MangaIntent,
private val settings: AppSettings,
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val localMangaRepository: LocalMangaRepository, private val localMangaRepository: LocalMangaRepository,
@ -82,7 +80,6 @@ class MangaDetailsDelegate(
branch: String?, branch: String?,
): List<ChapterListItem> { ): List<ChapterListItem> {
val result = ArrayList<ChapterListItem>(chapters.size) val result = ArrayList<ChapterListItem>(chapters.size)
val dateFormat = settings.getDateFormat()
val currentIndex = chapters.indexOfFirst { it.id == currentId } val currentIndex = chapters.indexOfFirst { it.id == currentId }
val firstNewIndex = chapters.size - newCount val firstNewIndex = chapters.size - newCount
val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id } val downloadedIds = downloadedChapters?.mapTo(HashSet(downloadedChapters.size)) { it.id }
@ -97,7 +94,6 @@ class MangaDetailsDelegate(
isNew = i >= firstNewIndex, isNew = i >= firstNewIndex,
isMissing = false, isMissing = false,
isDownloaded = downloadedIds?.contains(chapter.id) == true, isDownloaded = downloadedIds?.contains(chapter.id) == true,
dateFormat = dateFormat,
) )
} }
if (result.size < chapters.size / 2) { if (result.size < chapters.size / 2) {
@ -117,7 +113,6 @@ class MangaDetailsDelegate(
val result = ArrayList<ChapterListItem>(sourceChapters.size) val result = ArrayList<ChapterListItem>(sourceChapters.size)
val currentIndex = sourceChapters.indexOfFirst { it.id == currentId } val currentIndex = sourceChapters.indexOfFirst { it.id == currentId }
val firstNewIndex = sourceChapters.size - newCount val firstNewIndex = sourceChapters.size - newCount
val dateFormat = settings.getDateFormat()
for (i in sourceChapters.indices) { for (i in sourceChapters.indices) {
val chapter = sourceChapters[i] val chapter = sourceChapters[i]
val localChapter = chaptersMap.remove(chapter.id) val localChapter = chaptersMap.remove(chapter.id)
@ -130,14 +125,12 @@ class MangaDetailsDelegate(
isNew = i >= firstNewIndex, isNew = i >= firstNewIndex,
isMissing = false, isMissing = false,
isDownloaded = false, isDownloaded = false,
dateFormat = dateFormat,
) ?: chapter.toListItem( ) ?: chapter.toListItem(
isCurrent = i == currentIndex, isCurrent = i == currentIndex,
isUnread = i > currentIndex, isUnread = i > currentIndex,
isNew = i >= firstNewIndex, isNew = i >= firstNewIndex,
isMissing = true, isMissing = true,
isDownloaded = false, isDownloaded = false,
dateFormat = dateFormat,
) )
} }
if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source if (chaptersMap.isNotEmpty()) { // some chapters on device but not online source
@ -150,7 +143,6 @@ class MangaDetailsDelegate(
isNew = false, isNew = false,
isMissing = false, isMissing = false,
isDownloaded = false, isDownloaded = false,
dateFormat = dateFormat,
) )
} else { } else {
null null

@ -1,21 +1,24 @@
package org.koitharu.kotatsu.details.ui.model package org.koitharu.kotatsu.details.ui.model
import java.text.DateFormat import android.text.format.DateUtils
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
class ChapterListItem( class ChapterListItem(
val chapter: MangaChapter, val chapter: MangaChapter,
val flags: Int, val flags: Int,
private val uploadDateMs: Long, private val uploadDateMs: Long,
private val dateFormat: DateFormat,
) { ) {
var uploadDate: String? = null var uploadDate: CharSequence? = null
private set private set
get() { get() {
if (field != null) return field if (field != null) return field
if (uploadDateMs == 0L) return null if (uploadDateMs == 0L) return null
field = dateFormat.format(uploadDateMs) field = DateUtils.getRelativeTimeSpanString(
uploadDateMs,
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS,
)
return field return field
} }
@ -44,7 +47,6 @@ class ChapterListItem(
if (chapter != other.chapter) return false if (chapter != other.chapter) return false
if (flags != other.flags) return false if (flags != other.flags) return false
if (uploadDateMs != other.uploadDateMs) return false if (uploadDateMs != other.uploadDateMs) return false
if (dateFormat != other.dateFormat) return false
return true return true
} }
@ -53,7 +55,6 @@ class ChapterListItem(
var result = chapter.hashCode() var result = chapter.hashCode()
result = 31 * result + flags result = 31 * result + flags
result = 31 * result + uploadDateMs.hashCode() result = 31 * result + uploadDateMs.hashCode()
result = 31 * result + dateFormat.hashCode()
return result return result
} }

@ -1,6 +1,5 @@
package org.koitharu.kotatsu.details.ui.model package org.koitharu.kotatsu.details.ui.model
import java.text.DateFormat
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_CURRENT
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_DOWNLOADED
import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING import org.koitharu.kotatsu.details.ui.model.ChapterListItem.Companion.FLAG_MISSING
@ -14,7 +13,6 @@ fun MangaChapter.toListItem(
isNew: Boolean, isNew: Boolean,
isMissing: Boolean, isMissing: Boolean,
isDownloaded: Boolean, isDownloaded: Boolean,
dateFormat: DateFormat,
): ChapterListItem { ): ChapterListItem {
var flags = 0 var flags = 0
if (isCurrent) flags = flags or FLAG_CURRENT if (isCurrent) flags = flags or FLAG_CURRENT
@ -26,6 +24,5 @@ fun MangaChapter.toListItem(
chapter = this, chapter = this,
flags = flags, flags = flags,
uploadDateMs = uploadDate, uploadDateMs = uploadDate,
dateFormat = dateFormat,
) )
} }

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

@ -106,7 +106,7 @@ class ScrobblingInfoBottomSheet :
R.id.imageView_cover -> { R.id.imageView_cover -> {
val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return val coverUrl = viewModel.scrobblingInfo.value?.getOrNull(scrobblerIndex)?.coverUrl ?: return
val options = scaleUpActivityOptionsOf(v) val options = scaleUpActivityOptionsOf(v)
startActivity(ImageActivity.newIntent(v.context, coverUrl), options.toBundle()) startActivity(ImageActivity.newIntent(v.context, coverUrl, null), options.toBundle())
} }
} }
} }

@ -38,7 +38,6 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.copyToSuspending import org.koitharu.kotatsu.utils.ext.copyToSuspending
import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.progress.PausingProgressJob import org.koitharu.kotatsu.utils.progress.PausingProgressJob
import java.io.File import java.io.File
@ -118,7 +117,7 @@ class DownloadManager @AssistedInject constructor(
val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga val data = if (manga.chapters.isNullOrEmpty()) repo.getDetails(manga) else manga
output = CbzMangaOutput.get(destination, data) output = CbzMangaOutput.get(destination, data)
val coverUrl = data.largeCoverUrl ?: data.coverUrl 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)) output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
} }
val chapters = checkNotNull( val chapters = checkNotNull(
@ -139,7 +138,8 @@ class DownloadManager @AssistedInject constructor(
for ((pageIndex, page) in pages.withIndex()) { for ((pageIndex, page) in pages.withIndex()) {
runFailsafe(outState, pausingHandle) { runFailsafe(outState, pausingHandle) {
val url = repo.getPageUrl(page) 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( output.addPage(
chapter = chapter, chapter = chapter,
file = file, file = file,
@ -209,10 +209,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() val request = Request.Builder()
.url(url) .url(url)
.header(CommonHeaders.REFERER, referer) .header(CommonHeaders.REFERER, referer)
.tag(MangaSource::class.java, source)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED) .cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.get() .get()
.build() .build()
@ -242,7 +249,7 @@ class DownloadManager @AssistedInject constructor(
imageLoader.execute( imageLoader.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(manga.coverUrl) .data(manga.coverUrl)
.referer(manga.publicUrl) .tag(manga.source)
.size(coverWidth, coverHeight) .size(coverWidth, coverHeight)
.scale(Scale.FILL) .scale(Scale.FILL)
.build(), .build(),

@ -13,7 +13,10 @@ import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.parsers.util.format 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
fun downloadItemAD( fun downloadItemAD(
scope: CoroutineScope, scope: CoroutineScope,
@ -40,8 +43,7 @@ fun downloadItemAD(
bind { bind {
job?.cancel() job?.cancel()
job = item.progressAsFlow().onFirst { state -> 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) placeholder(state.cover)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
@ -60,6 +62,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false binding.buttonResume.isVisible = false
} }
is DownloadState.Done -> { is DownloadState.Done -> {
binding.textViewStatus.setText(R.string.download_complete) binding.textViewStatus.setText(R.string.download_complete)
binding.progressBar.isIndeterminate = false binding.progressBar.isIndeterminate = false
@ -69,6 +72,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false binding.buttonResume.isVisible = false
} }
is DownloadState.Error -> { is DownloadState.Error -> {
binding.textViewStatus.setText(R.string.error_occurred) binding.textViewStatus.setText(R.string.error_occurred)
binding.progressBar.isIndeterminate = false binding.progressBar.isIndeterminate = false
@ -79,6 +83,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = state.canRetry binding.buttonCancel.isVisible = state.canRetry
binding.buttonResume.isVisible = state.canRetry binding.buttonResume.isVisible = state.canRetry
} }
is DownloadState.PostProcessing -> { is DownloadState.PostProcessing -> {
binding.textViewStatus.setText(R.string.processing_) binding.textViewStatus.setText(R.string.processing_)
binding.progressBar.isIndeterminate = true binding.progressBar.isIndeterminate = true
@ -88,6 +93,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = false binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false binding.buttonResume.isVisible = false
} }
is DownloadState.Preparing -> { is DownloadState.Preparing -> {
binding.textViewStatus.setText(R.string.preparing_) binding.textViewStatus.setText(R.string.preparing_)
binding.progressBar.isIndeterminate = true binding.progressBar.isIndeterminate = true
@ -97,6 +103,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = true binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false binding.buttonResume.isVisible = false
} }
is DownloadState.Progress -> { is DownloadState.Progress -> {
binding.textViewStatus.setText(R.string.manga_downloading_) binding.textViewStatus.setText(R.string.manga_downloading_)
binding.progressBar.isIndeterminate = false binding.progressBar.isIndeterminate = false
@ -109,6 +116,7 @@ fun downloadItemAD(
binding.buttonCancel.isVisible = true binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false binding.buttonResume.isVisible = false
} }
is DownloadState.Queued -> { is DownloadState.Queued -> {
binding.textViewStatus.setText(R.string.queued) binding.textViewStatus.setText(R.string.queued)
binding.progressBar.isIndeterminate = false binding.progressBar.isIndeterminate = false

@ -76,7 +76,7 @@ fun exploreSourceListItemAD(
bind { bind {
binding.textViewTitle.text = item.source.title binding.textViewTitle.text = item.source.title
val fallbackIcon = FaviconFallbackDrawable(context, item.source.name) val fallbackIcon = FaviconFallbackDrawable(context, item.source.name)
binding.imageViewIcon.newImageRequest(item.source.faviconUri())?.run { binding.imageViewIcon.newImageRequest(item.source.faviconUri(), item.source)?.run {
fallback(fallbackIcon) fallback(fallbackIcon)
placeholder(fallbackIcon) placeholder(fallbackIcon)
error(fallbackIcon) error(fallbackIcon)

@ -5,7 +5,9 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View 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.graphics.ColorUtils
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ImageViewCompat import androidx.core.widget.ImageViewCompat
@ -16,7 +18,11 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemCategoryBinding import org.koitharu.kotatsu.databinding.ItemCategoryBinding
import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener import org.koitharu.kotatsu.favourites.ui.categories.FavouriteCategoriesListListener
import org.koitharu.kotatsu.list.ui.model.ListModel 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( fun categoryAD(
coil: ImageLoader, coil: ImageLoader,

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

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

@ -15,7 +15,6 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.image.CoverSizeResolver import org.koitharu.kotatsu.utils.image.CoverSizeResolver
fun mangaGridItemAD( fun mangaGridItemAD(
@ -39,8 +38,7 @@ fun mangaGridItemAD(
bind { payloads -> bind { payloads ->
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads) 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)) size(CoverSizeResolver(binding.imageViewCover))
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)

@ -17,7 +17,6 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.disposeImageRequest import org.koitharu.kotatsu.utils.ext.disposeImageRequest
import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.enqueueWith
import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.newImageRequest
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.textAndVisible import org.koitharu.kotatsu.utils.ext.textAndVisible
import org.koitharu.kotatsu.utils.image.CoverSizeResolver import org.koitharu.kotatsu.utils.image.CoverSizeResolver
@ -52,8 +51,7 @@ fun mangaListDetailedItemAD(
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle binding.textViewSubtitle.textAndVisible = item.subtitle
binding.progressView.setPercent(item.progress, MangaListAdapter.PAYLOAD_PROGRESS in payloads) 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)) size(CoverSizeResolver(binding.imageViewCover))
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)

@ -10,7 +10,10 @@ import org.koitharu.kotatsu.databinding.ItemMangaListBinding
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.MangaListModel import org.koitharu.kotatsu.list.ui.model.MangaListModel
import org.koitharu.kotatsu.parsers.model.Manga 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.textAndVisible
fun mangaListItemAD( fun mangaListItemAD(
coil: ImageLoader, coil: ImageLoader,
@ -31,8 +34,7 @@ fun mangaListItemAD(
bind { bind {
binding.textViewTitle.text = item.title binding.textViewTitle.text = item.title
binding.textViewSubtitle.textAndVisible = item.subtitle 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) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)

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

@ -26,7 +26,6 @@ import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.asArrayList import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.report import org.koitharu.kotatsu.utils.ext.report
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import javax.inject.Inject import javax.inject.Inject
@ -99,7 +98,10 @@ class ImportService : CoroutineIntentService() {
if (manga != null) { if (manga != null) {
notification.setLargeIcon( notification.setLargeIcon(
coil.execute( coil.execute(
ImageRequest.Builder(applicationContext).data(manga.coverUrl).referer(manga.publicUrl).build(), ImageRequest.Builder(applicationContext)
.data(manga.coverUrl)
.tag(manga.source)
.build(),
).toBitmapOrNull(), ).toBitmapOrNull(),
) )
notification.setSubText(manga.title) notification.setSubText(manga.title)

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

@ -6,8 +6,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlin.math.roundToInt
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseBottomSheet import org.koitharu.kotatsu.base.ui.BaseBottomSheet
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
@ -21,6 +19,8 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback import org.koitharu.kotatsu.utils.RecyclerViewScrollCallback
import org.koitharu.kotatsu.utils.ext.getParcelableCompat import org.koitharu.kotatsu.utils.ext.getParcelableCompat
import org.koitharu.kotatsu.utils.ext.withArgs import org.koitharu.kotatsu.utils.ext.withArgs
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemClickListener<ChapterListItem> { class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemClickListener<ChapterListItem> {
@ -41,7 +41,6 @@ class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemC
} }
val currentId = requireArguments().getLong(ARG_CURRENT_ID, 0L) val currentId = requireArguments().getLong(ARG_CURRENT_ID, 0L)
val currentPosition = chapters.indexOfFirst { it.id == currentId } val currentPosition = chapters.indexOfFirst { it.id == currentId }
val dateFormat = settings.getDateFormat()
val items = chapters.mapIndexed { index, chapter -> val items = chapters.mapIndexed { index, chapter ->
chapter.toListItem( chapter.toListItem(
isCurrent = index == currentPosition, isCurrent = index == currentPosition,
@ -49,7 +48,6 @@ class ChaptersBottomSheet : BaseBottomSheet<SheetChaptersBinding>(), OnListItemC
isNew = false, isNew = false,
isMissing = false, isMissing = false,
isDownloaded = false, isDownloaded = false,
dateFormat = dateFormat,
) )
} }
binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter -> binding.recyclerView.adapter = ChaptersAdapter(this).also { adapter ->

@ -13,11 +13,9 @@ import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Scale import coil.size.Scale
import coil.size.ViewSizeResolver 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.LabelFormatter
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
@ -27,7 +25,13 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.reader.domain.ReaderColorFilter 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.setValueRounded
import javax.inject.Inject
import com.google.android.material.R as materialR
@AndroidEntryPoint @AndroidEntryPoint
class ColorFilterConfigActivity : class ColorFilterConfigActivity :
@ -112,9 +116,9 @@ class ColorFilterConfigActivity :
if (preview == null) return if (preview == null) return
ImageRequest.Builder(this@ColorFilterConfigActivity) ImageRequest.Builder(this@ColorFilterConfigActivity)
.data(preview.url) .data(preview.url)
.referer(preview.referer)
.scale(Scale.FILL) .scale(Scale.FILL)
.decodeRegion() .decodeRegion()
.tag(preview.source)
.error(R.drawable.ic_error_placeholder) .error(R.drawable.ic_error_placeholder)
.size(ViewSizeResolver(binding.imageViewBefore)) .size(ViewSizeResolver(binding.imageViewBefore))
.allowRgb565(false) .allowRgb565(false)

@ -5,9 +5,12 @@ import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Scale import coil.size.Scale
import coil.size.Size import coil.size.Size
import com.google.android.material.R as materialR
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding 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.R
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.databinding.ItemPageThumbBinding import org.koitharu.kotatsu.databinding.ItemPageThumbBinding
@ -16,9 +19,9 @@ import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.decodeRegion import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.isLowRamDevice 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.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.setTextColorAttr import org.koitharu.kotatsu.utils.ext.setTextColorAttr
import com.google.android.material.R as materialR
fun pageThumbnailAD( fun pageThumbnailAD(
coil: ImageLoader, coil: ImageLoader,
@ -40,7 +43,7 @@ fun pageThumbnailAD(
coil.execute( coil.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(url) .data(url)
.referer(item.page.referer) .tag(item.page.source)
.size(thumbSize) .size(thumbSize)
.scale(Scale.FILL) .scale(Scale.FILL)
.allowRgb565(true) .allowRgb565(true)

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

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

@ -29,7 +29,6 @@ import org.koitharu.kotatsu.utils.ext.getLocalesConfig
import org.koitharu.kotatsu.utils.ext.map import org.koitharu.kotatsu.utils.ext.map
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.toList import org.koitharu.kotatsu.utils.ext.toList
import java.util.Date
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -55,20 +54,6 @@ class AppearanceSettingsFragment :
entryValues = ListMode.values().names() entryValues = ListMode.values().names()
setDefaultValueCompat(ListMode.GRID.name) setDefaultValueCompat(ListMode.GRID.name)
} }
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
entryValues = resources.getStringArray(R.array.date_formats)
val now = Date().time
entries = entryValues.map { value ->
val formattedDate = settings.getDateFormat(value.toString()).format(now)
if (value == "") {
getString(R.string.default_s, formattedDate)
} else {
formattedDate
}
}.toTypedArray()
setDefaultValueCompat("")
summary = "%s"
}
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP) findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
?.isChecked = !settings.appPassword.isNullOrEmpty() ?.isChecked = !settings.appPassword.isNullOrEmpty()
findPreference<ActivityListPreference>(AppSettings.KEY_APP_LOCALE)?.run { findPreference<ActivityListPreference>(AppSettings.KEY_APP_LOCALE)?.run {

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

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

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

@ -28,7 +28,7 @@ fun feedItemAD(
bind { bind {
binding.textViewTitle.isBold = item.isNew binding.textViewTitle.isBold = item.isNew
binding.textViewSummary.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) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)

@ -46,7 +46,6 @@ import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.Tracker
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground import org.koitharu.kotatsu.utils.ext.trySetForeground
@ -155,7 +154,10 @@ class TrackWorker @AssistedInject constructor(
setNumber(newChapters.size) setNumber(newChapters.size)
setLargeIcon( setLargeIcon(
coil.execute( coil.execute(
ImageRequest.Builder(applicationContext).data(manga.coverUrl).referer(manga.publicUrl).build(), ImageRequest.Builder(applicationContext)
.data(manga.coverUrl)
.tag(manga.source)
.build(),
).toBitmapOrNull(), ).toBitmapOrNull(),
) )
setSmallIcon(R.drawable.ic_stat_book_plus) setSmallIcon(R.drawable.ic_stat_book_plus)

@ -10,19 +10,19 @@ import coil.request.ImageResult
import coil.request.SuccessResult 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 okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.koitharu.kotatsu.R 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.image.RegionBitmapDecoder
import org.koitharu.kotatsu.utils.progress.ImageRequestIndicatorListener 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) val current = CoilUtils.result(this)
if (current != null && current.request.data == url) { if (current != null && current.request.data == url) {
return null return null
} }
return ImageRequest.Builder(context) return ImageRequest.Builder(context)
.data(url) .data(url)
.tag(mangaSource)
.crossfade(context) .crossfade(context)
.target(this) .target(this)
} }
@ -45,22 +45,8 @@ fun ImageResult.toBitmapOrNull() = when (this) {
} catch (_: Throwable) { } catch (_: Throwable) {
null null
} }
is ErrorResult -> null
}
fun ImageRequest.Builder.referer(referer: String): ImageRequest.Builder { is ErrorResult -> null
if (referer.isEmpty()) {
return this
}
try {
setHeader(CommonHeaders.REFERER, referer)
} catch (e: IllegalArgumentException) {
val baseUrl = referer.baseUrl()
if (baseUrl != null) {
setHeader(CommonHeaders.REFERER, baseUrl)
}
}
return this
} }
fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRequest.Builder { fun ImageRequest.Builder.indicator(indicator: BaseProgressIndicator<*>): ImageRequest.Builder {
@ -80,11 +66,3 @@ fun ImageRequest.Builder.crossfade(context: Context?): ImageRequest.Builder {
val duration = context.resources.getInteger(R.integer.config_defaultAnimTime) * context.animatorDurationScale val duration = context.resources.getInteger(R.integer.config_defaultAnimTime) * context.animatorDurationScale
return crossfade(duration.toInt()) return crossfade(duration.toInt())
} }
private fun String.baseUrl(): String? {
return (this.toHttpUrlOrNull()?.newBuilder("/") ?: return null)
.username("")
.password("")
.build()
.toString()
}

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

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

@ -8,7 +8,7 @@
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:orientation="vertical" android:orientation="vertical"
android:padding="6dp" android:padding="6dp"
tools:theme="@style/Theme.Kotatsu.Mint"> tools:theme="@style/Theme.Kotatsu.Miku">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/card" android:id="@+id/card"

@ -61,7 +61,6 @@
android:clipToPadding="false" android:clipToPadding="false"
android:paddingStart="0dp" android:paddingStart="0dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:scrollIndicators="start|end"
android:scrollbars="none" android:scrollbars="none"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Colored themes --> <!-- Colored themes -->
<style name="Theme.Kotatsu.Mint"> <style name="Theme.Kotatsu.Miku">
<item name="colorPrimary">#4CDBCE</item> <item name="colorPrimary">#4CDBCE</item>
<item name="colorOnPrimary">#003733</item> <item name="colorOnPrimary">#003733</item>
<item name="colorPrimaryContainer">#00504A</item> <item name="colorPrimaryContainer">#00504A</item>
@ -30,7 +30,7 @@
<item name="colorPrimaryInverse">#006A63</item> <item name="colorPrimaryInverse">#006A63</item>
</style> </style>
<style name="Theme.Kotatsu.October"> <style name="Theme.Kotatsu.Asuka">
<item name="colorPrimary">#FFB3AF</item> <item name="colorPrimary">#FFB3AF</item>
<item name="colorOnPrimary">#68000E</item> <item name="colorOnPrimary">#68000E</item>
<item name="colorPrimaryContainer">#930018</item> <item name="colorPrimaryContainer">#930018</item>
@ -58,4 +58,91 @@
<item name="colorSurfaceInverse">#EDE0DE</item> <item name="colorSurfaceInverse">#EDE0DE</item>
<item name="colorPrimaryInverse">#BA1928</item> <item name="colorPrimaryInverse">#BA1928</item>
</style> </style>
<style name="Theme.Kotatsu.Mion">
<item name="colorPrimary">#7FDA8E</item>
<item name="colorOnPrimary">#003915</item>
<item name="colorPrimaryContainer">#005321</item>
<item name="colorOnPrimaryContainer">#9AF7A8</item>
<item name="colorSecondary">#EAC32E</item>
<item name="colorOnSecondary">#3C2F00</item>
<item name="colorSecondaryContainer">#564500</item>
<item name="colorOnSecondaryContainer">#FFE080</item>
<item name="colorTertiary">#5AD5F9</item>
<item name="colorOnTertiary">#003542</item>
<item name="colorTertiaryContainer">#004E5F</item>
<item name="colorOnTertiaryContainer">#B4EBFF</item>
<item name="colorError">#FFB4AB</item>
<item name="colorErrorContainer">#93000A</item>
<item name="colorOnError">#690005</item>
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="android:colorBackground">#1A1C19</item>
<item name="colorOnBackground">#E2E3DD</item>
<item name="colorSurface">#1A1C19</item>
<item name="colorOnSurface">#E2E3DD</item>
<item name="colorSurfaceVariant">#414941</item>
<item name="colorOnSurfaceVariant">#C1C9BE</item>
<item name="colorOutline">#8B9389</item>
<item name="colorOnSurfaceInverse">#1A1C19</item>
<item name="colorSurfaceInverse">#E2E3DD</item>
<item name="colorPrimaryInverse">#006E2F</item>
</style>
<style name="Theme.Kotatsu.Rikka">
<item name="colorPrimary">#C2C1FF</item>
<item name="colorOnPrimary">#1800A7</item>
<item name="colorPrimaryContainer">#2C1BD7</item>
<item name="colorOnPrimaryContainer">#E2DFFF</item>
<item name="colorSecondary">#F7ACFF</item>
<item name="colorOnSecondary">#560067</item>
<item name="colorSecondaryContainer">#761789</item>
<item name="colorOnSecondaryContainer">#FFD6FF</item>
<item name="colorTertiary">#E9B9D2</item>
<item name="colorOnTertiary">#47263A</item>
<item name="colorTertiaryContainer">#5F3C51</item>
<item name="colorOnTertiaryContainer">#FFD8EB</item>
<item name="colorError">#FFB4AB</item>
<item name="colorErrorContainer">#93000A</item>
<item name="colorOnError">#690005</item>
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="android:colorBackground">#1C1B1F</item>
<item name="colorOnBackground">#E5E1E6</item>
<item name="colorSurface">#1C1B1F</item>
<item name="colorOnSurface">#E5E1E6</item>
<item name="colorSurfaceVariant">#47464F</item>
<item name="colorOnSurfaceVariant">#C8C5D0</item>
<item name="colorOutline">#918F9A</item>
<item name="colorOnSurfaceInverse">#1C1B1F</item>
<item name="colorSurfaceInverse">#E5E1E6</item>
<item name="colorPrimaryInverse">#4841EE</item>
</style>
<style name="Theme.Kotatsu.Sakura">
<item name="colorPrimary">#FFB1C8</item>
<item name="colorOnPrimary">#650033</item>
<item name="colorPrimaryContainer">#8E004A</item>
<item name="colorOnPrimaryContainer">#FFD9E2</item>
<item name="colorSecondary">#FFB5A0</item>
<item name="colorOnSecondary">#601500</item>
<item name="colorSecondaryContainer">#872100</item>
<item name="colorOnSecondaryContainer">#FFDBD1</item>
<item name="colorTertiary">#EFBD94</item>
<item name="colorOnTertiary">#48290B</item>
<item name="colorTertiaryContainer">#613F20</item>
<item name="colorOnTertiaryContainer">#FFDCC1</item>
<item name="colorError">#FFB4AB</item>
<item name="colorErrorContainer">#93000A</item>
<item name="colorOnError">#690005</item>
<item name="colorOnErrorContainer">#FFDAD6</item>
<item name="android:colorBackground">#201A1B</item>
<item name="colorOnBackground">#EBE0E1</item>
<item name="colorSurface">#201A1B</item>
<item name="colorOnSurface">#EBE0E1</item>
<item name="colorSurfaceVariant">#514347</item>
<item name="colorOnSurfaceVariant">#D5C2C6</item>
<item name="colorOutline">#9E8C90</item>
<item name="colorOnSurfaceInverse">#201A1B</item>
<item name="colorSurfaceInverse">#EBE0E1</item>
<item name="colorPrimaryInverse">#B31A62</item>
</style>
</resources> </resources>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- Colored themes --> <!-- Colored themes -->
<style name="Theme.Kotatsu.Mint"> <style name="Theme.Kotatsu.Miku">
<item name="colorPrimary">#006A63</item> <item name="colorPrimary">#006A63</item>
<item name="colorOnPrimary">#FFFFFF</item> <item name="colorOnPrimary">#FFFFFF</item>
<item name="colorPrimaryContainer">#6EF8EA</item> <item name="colorPrimaryContainer">#6EF8EA</item>
@ -30,7 +30,7 @@
<item name="colorPrimaryInverse">#4CDBCE</item> <item name="colorPrimaryInverse">#4CDBCE</item>
</style> </style>
<style name="Theme.Kotatsu.October"> <style name="Theme.Kotatsu.Asuka">
<item name="colorPrimary">#BA1928</item> <item name="colorPrimary">#BA1928</item>
<item name="colorOnPrimary">#FFFFFF</item> <item name="colorOnPrimary">#FFFFFF</item>
<item name="colorPrimaryContainer">#FFDAD7</item> <item name="colorPrimaryContainer">#FFDAD7</item>
@ -58,4 +58,91 @@
<item name="colorSurfaceInverse">#362F2E</item> <item name="colorSurfaceInverse">#362F2E</item>
<item name="colorPrimaryInverse">#FFB3AF</item> <item name="colorPrimaryInverse">#FFB3AF</item>
</style> </style>
<style name="Theme.Kotatsu.Mion">
<item name="colorPrimary">#006E2F</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="colorPrimaryContainer">#9AF7A8</item>
<item name="colorOnPrimaryContainer">#002109</item>
<item name="colorSecondary">#725C00</item>
<item name="colorOnSecondary">#FFFFFF</item>
<item name="colorSecondaryContainer">#FFE080</item>
<item name="colorOnSecondaryContainer">#231B00</item>
<item name="colorTertiary">#00677E</item>
<item name="colorOnTertiary">#FFFFFF</item>
<item name="colorTertiaryContainer">#B4EBFF</item>
<item name="colorOnTertiaryContainer">#001F27</item>
<item name="colorError">#BA1A1A</item>
<item name="colorErrorContainer">#FFDAD6</item>
<item name="colorOnError">#FFFFFF</item>
<item name="colorOnErrorContainer">#410002</item>
<item name="android:colorBackground">#FCFDF7</item>
<item name="colorOnBackground">#1A1C19</item>
<item name="colorSurface">#FCFDF7</item>
<item name="colorOnSurface">#1A1C19</item>
<item name="colorSurfaceVariant">#DDE5DA</item>
<item name="colorOnSurfaceVariant">#414941</item>
<item name="colorOutline">#727970</item>
<item name="colorOnSurfaceInverse">#F0F1EC</item>
<item name="colorSurfaceInverse">#2E312E</item>
<item name="colorPrimaryInverse">#7FDA8E</item>
</style>
<style name="Theme.Kotatsu.Rikka">
<item name="colorPrimary">#4841EE</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="colorPrimaryContainer">#E2DFFF</item>
<item name="colorOnPrimaryContainer">#0B006B</item>
<item name="colorSecondary">#9135A3</item>
<item name="colorOnSecondary">#FFFFFF</item>
<item name="colorSecondaryContainer">#FFD6FF</item>
<item name="colorOnSecondaryContainer">#350040</item>
<item name="colorTertiary">#795369</item>
<item name="colorOnTertiary">#FFFFFF</item>
<item name="colorTertiaryContainer">#FFD8EB</item>
<item name="colorOnTertiaryContainer">#2F1124</item>
<item name="colorError">#BA1A1A</item>
<item name="colorErrorContainer">#FFDAD6</item>
<item name="colorOnError">#FFFFFF</item>
<item name="colorOnErrorContainer">#410002</item>
<item name="android:colorBackground">#FFFBFF</item>
<item name="colorOnBackground">#1C1B1F</item>
<item name="colorSurface">#FFFBFF</item>
<item name="colorOnSurface">#1C1B1F</item>
<item name="colorSurfaceVariant">#E4E1EC</item>
<item name="colorOnSurfaceVariant">#47464F</item>
<item name="colorOutline">#777680</item>
<item name="colorOnSurfaceInverse">#F3EFF4</item>
<item name="colorSurfaceInverse">#313034</item>
<item name="colorPrimaryInverse">#C2C1FF</item>
</style>
<style name="Theme.Kotatsu.Sakura">
<item name="colorPrimary">#B31A62</item>
<item name="colorOnPrimary">#FFFFFF</item>
<item name="colorPrimaryContainer">#FFD9E2</item>
<item name="colorOnPrimaryContainer">#3E001D</item>
<item name="colorSecondary">#B12E00</item>
<item name="colorOnSecondary">#FFFFFF</item>
<item name="colorSecondaryContainer">#FFDBD1</item>
<item name="colorOnSecondaryContainer">#3B0900</item>
<item name="colorTertiary">#7C5635</item>
<item name="colorOnTertiary">#FFFFFF</item>
<item name="colorTertiaryContainer">#FFDCC1</item>
<item name="colorOnTertiaryContainer">#2E1500</item>
<item name="colorError">#BA1A1A</item>
<item name="colorErrorContainer">#FFDAD6</item>
<item name="colorOnError">#FFFFFF</item>
<item name="colorOnErrorContainer">#410002</item>
<item name="android:colorBackground">#FFFBFF</item>
<item name="colorOnBackground">#201A1B</item>
<item name="colorSurface">#FFFBFF</item>
<item name="colorOnSurface">#201A1B</item>
<item name="colorSurfaceVariant">#F2DDE1</item>
<item name="colorOnSurfaceVariant">#514347</item>
<item name="colorOutline">#837377</item>
<item name="colorOnSurfaceInverse">#FAEEEF</item>
<item name="colorSurfaceInverse">#352F30</item>
<item name="colorPrimaryInverse">#FFB1C8</item>
</style>
</resources> </resources>

@ -1,7 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Kotatsu.Mint" /> <style name="Theme.Kotatsu.Miku" />
<style name="Theme.Kotatsu.October" /> <style name="Theme.Kotatsu.Asuka" />
<style name="Theme.Kotatsu.Mion" />
<style name="Theme.Kotatsu.Rikka" />
<style name="Theme.Kotatsu.Sakura" />
</resources> </resources>

@ -38,12 +38,4 @@
<item>2</item> <item>2</item>
<item>0</item> <item>0</item>
</string-array> </string-array>
<string-array name="date_formats">
<item />
<item>MM/dd/yy</item>
<item>dd/MM/yy</item>
<item>yyyy-MM-dd</item>
<item>dd MMM yyyy</item>
<item>MMM dd, yyyy</item>
</string-array>
</resources> </resources>

@ -408,9 +408,12 @@
<string name="enable_logging">Enable logging</string> <string name="enable_logging">Enable logging</string>
<string name="enable_logging_summary">Record some actions for debug purposes</string> <string name="enable_logging_summary">Record some actions for debug purposes</string>
<string name="show_suspicious_content">Show suspicious content</string> <string name="show_suspicious_content">Show suspicious content</string>
<string name="theme_name_mint">Mint</string>
<string name="theme_name_dynamic">Dynamic</string> <string name="theme_name_dynamic">Dynamic</string>
<string name="color_theme">Color scheme</string> <string name="color_theme">Color scheme</string>
<string name="theme_name_october">October</string>
<string name="show_in_grid_view">Show in grid view</string> <string name="show_in_grid_view">Show in grid view</string>
<string name="theme_name_miku">Miku</string>
<string name="theme_name_asuka">Asuka</string>
<string name="theme_name_mion">Mion</string>
<string name="theme_name_rikka">Rikka</string>
<string name="theme_name_sakura">Sakura</string>
</resources> </resources>

@ -27,10 +27,6 @@
android:title="@string/language" android:title="@string/language"
app:allowDividerAbove="true" /> app:allowDividerAbove="true" />
<ListPreference
android:key="date_format"
android:title="@string/date_format" />
<ListPreference <ListPreference
android:entries="@array/list_modes" android:entries="@array/list_modes"
android:key="list_mode_2" android:key="list_mode_2"

@ -12,7 +12,7 @@ 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
class DummyParser(override val context: MangaLoaderContext) : MangaParser(MangaSource.DUMMY) { class DummyParser(context: MangaLoaderContext) : MangaParser(context, MangaSource.DUMMY) {
override val configKeyDomain: ConfigKey.Domain override val configKeyDomain: ConfigKey.Domain
get() = ConfigKey.Domain("localhost", null) get() = ConfigKey.Domain("localhost", null)

Loading…
Cancel
Save