Images proxy #357
This commit is contained in:
@@ -45,7 +45,6 @@ import org.koitharu.kotatsu.list.domain.ListExtraProviderImpl
|
|||||||
import org.koitharu.kotatsu.local.data.CacheDir
|
import org.koitharu.kotatsu.local.data.CacheDir
|
||||||
import org.koitharu.kotatsu.local.data.CbzFetcher
|
import org.koitharu.kotatsu.local.data.CbzFetcher
|
||||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
|
||||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
@@ -91,7 +90,8 @@ interface AppModule {
|
|||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
@MangaHttpClient okHttpClient: OkHttpClient,
|
@MangaHttpClient okHttpClient: OkHttpClient,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
pagesCache: PagesCache,
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
): ImageLoader {
|
): ImageLoader {
|
||||||
val diskCacheFactory = {
|
val diskCacheFactory = {
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||||
@@ -113,7 +113,8 @@ interface AppModule {
|
|||||||
.add(SvgDecoder.Factory())
|
.add(SvgDecoder.Factory())
|
||||||
.add(CbzFetcher.Factory())
|
.add(CbzFetcher.Factory())
|
||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
||||||
.add(MangaPageFetcher.Factory(context, okHttpClient, pagesCache, mangaRepositoryFactory))
|
.add(pageFetcherFactory)
|
||||||
|
.add(imageProxyInterceptor)
|
||||||
.build(),
|
.build(),
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import coil.intercept.Interceptor
|
||||||
|
import coil.request.ErrorResult
|
||||||
|
import coil.request.ImageResult
|
||||||
|
import coil.request.SuccessResult
|
||||||
|
import coil.size.Dimension
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isHttpOrHttps
|
||||||
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import java.util.Collections
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ImageProxyInterceptor @Inject constructor(
|
||||||
|
private val settings: AppSettings,
|
||||||
|
) : Interceptor {
|
||||||
|
|
||||||
|
private val blacklist = Collections.synchronizedSet(ArraySet<String>())
|
||||||
|
|
||||||
|
override suspend fun intercept(chain: Interceptor.Chain): ImageResult {
|
||||||
|
val request = chain.request
|
||||||
|
if (!settings.isImagesProxyEnabled) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
val url: HttpUrl? = when (val data = request.data) {
|
||||||
|
is HttpUrl -> data
|
||||||
|
is String -> data.toHttpUrlOrNull()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (url == null || !url.isHttpOrHttps || url.host in blacklist) {
|
||||||
|
return chain.proceed(request)
|
||||||
|
}
|
||||||
|
val newUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", url.toString())
|
||||||
|
.addQueryParameter("fit", "outside")
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val size = request.sizeResolver.size()
|
||||||
|
(size.height as? Dimension.Pixels)?.let { newUrl.addQueryParameter("h", it.toString()) }
|
||||||
|
(size.width as? Dimension.Pixels)?.let { newUrl.addQueryParameter("w", it.toString()) }
|
||||||
|
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.data(newUrl.build())
|
||||||
|
.build()
|
||||||
|
val result = chain.proceed(newRequest)
|
||||||
|
return if (result is SuccessResult) {
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
logDebug((result as? ErrorResult)?.throwable)
|
||||||
|
chain.proceed(request).also {
|
||||||
|
if (it is SuccessResult) {
|
||||||
|
blacklist.add(url.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun interceptPageRequest(request: Request, okHttp: OkHttpClient): Response {
|
||||||
|
if (!settings.isImagesProxyEnabled) {
|
||||||
|
return okHttp.newCall(request).await()
|
||||||
|
}
|
||||||
|
val sourceUrl = request.url
|
||||||
|
val targetUrl = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host("wsrv.nl")
|
||||||
|
.addQueryParameter("url", sourceUrl.toString())
|
||||||
|
.addQueryParameter("we", null)
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.url(targetUrl.build())
|
||||||
|
.build()
|
||||||
|
return runCatchingCancellable {
|
||||||
|
okHttp.doCall(newRequest)
|
||||||
|
}.recover {
|
||||||
|
logDebug(it)
|
||||||
|
okHttp.doCall(request).also {
|
||||||
|
blacklist.add(sourceUrl.host)
|
||||||
|
}
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun OkHttpClient.doCall(request: Request): Response {
|
||||||
|
return newCall(request).await().ensureSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logDebug(e: Throwable?) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.w("ImageProxy", e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -277,6 +277,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
val isReaderSliderEnabled: Boolean
|
val isReaderSliderEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
get() = prefs.getBoolean(KEY_READER_SLIDER, true)
|
||||||
|
|
||||||
|
val isImagesProxyEnabled: Boolean
|
||||||
|
get() = prefs.getBoolean(KEY_IMAGES_PROXY, false)
|
||||||
|
|
||||||
val dnsOverHttps: DoHProvider
|
val dnsOverHttps: DoHProvider
|
||||||
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
get() = prefs.getEnumValue(KEY_DOH, DoHProvider.NONE)
|
||||||
|
|
||||||
@@ -448,6 +451,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
|||||||
const val KEY_PROXY_TYPE = "proxy_type"
|
const val KEY_PROXY_TYPE = "proxy_type"
|
||||||
const val KEY_PROXY_ADDRESS = "proxy_address"
|
const val KEY_PROXY_ADDRESS = "proxy_address"
|
||||||
const val KEY_PROXY_PORT = "proxy_port"
|
const val KEY_PROXY_PORT = "proxy_port"
|
||||||
|
const val KEY_IMAGES_PROXY = "images_proxy"
|
||||||
|
|
||||||
// About
|
// About
|
||||||
const val KEY_APP_UPDATE = "app_update"
|
const val KEY_APP_UPDATE = "app_update"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.core.util.ext
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -23,3 +24,17 @@ fun Response.parseJsonOrNull(): JSONObject? {
|
|||||||
closeQuietly()
|
closeQuietly()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val HttpUrl.isHttpOrHttps: Boolean
|
||||||
|
get() {
|
||||||
|
val s = scheme.lowercase()
|
||||||
|
return s == "https" || s == "http"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Response.ensureSuccess() = apply {
|
||||||
|
if (!isSuccessful || code == HttpURLConnection.HTTP_NO_CONTENT) {
|
||||||
|
val message = "Invalid response: $code $message at ${request.url}"
|
||||||
|
closeQuietly()
|
||||||
|
throw IllegalStateException(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.koitharu.kotatsu.core.model.findChapter
|
import org.koitharu.kotatsu.core.model.findChapter
|
||||||
|
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
@@ -14,7 +15,6 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
|
|||||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
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.await
|
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||||
@@ -28,6 +28,7 @@ class DetectReaderModeUseCase @Inject constructor(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||||
|
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend operator fun invoke(manga: Manga, state: ReaderState?): ReaderMode {
|
suspend operator fun invoke(manga: Manga, state: ReaderState?): ReaderMode {
|
||||||
@@ -70,7 +71,7 @@ class DetectReaderModeUseCase @Inject constructor(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val request = PageLoader.createPageRequest(page, url)
|
val request = PageLoader.createPageRequest(page, url)
|
||||||
okHttpClient.newCall(request).await().use {
|
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use {
|
||||||
runInterruptible(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
getBitmapSize(it.body?.byteStream())
|
getBitmapSize(it.body?.byteStream())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,19 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
|
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||||
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
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.util.await
|
|
||||||
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
|
||||||
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -51,6 +52,7 @@ class PageLoader @Inject constructor(
|
|||||||
private val cache: PagesCache,
|
private val cache: PagesCache,
|
||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
) : RetainedLifecycle.OnClearedListener {
|
) : RetainedLifecycle.OnClearedListener {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -191,10 +193,7 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val request = createPageRequest(page, pageUrl)
|
val request = createPageRequest(page, pageUrl)
|
||||||
okHttp.newCall(request).await().use { response ->
|
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||||
check(response.isSuccessful) {
|
|
||||||
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
|
||||||
}
|
|
||||||
val body = checkNotNull(response.body) {
|
val body = checkNotNull(response.body) {
|
||||||
"Null response"
|
"Null response"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,21 +9,24 @@ import coil.fetch.FetchResult
|
|||||||
import coil.fetch.Fetcher
|
import coil.fetch.Fetcher
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okio.Path.Companion.toOkioPath
|
import okio.Path.Companion.toOkioPath
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
|
import org.koitharu.kotatsu.core.network.ImageProxyInterceptor
|
||||||
|
import org.koitharu.kotatsu.core.network.MangaHttpClient
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.local.data.CbzFilter
|
import org.koitharu.kotatsu.local.data.CbzFilter
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.util.await
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mimeType
|
import org.koitharu.kotatsu.parsers.util.mimeType
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MangaPageFetcher(
|
class MangaPageFetcher(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -32,6 +35,7 @@ class MangaPageFetcher(
|
|||||||
private val options: Options,
|
private val options: Options,
|
||||||
private val page: MangaPage,
|
private val page: MangaPage,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
@@ -66,7 +70,7 @@ class MangaPageFetcher(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val request = PageLoader.createPageRequest(page, pageUrl)
|
val request = PageLoader.createPageRequest(page, pageUrl)
|
||||||
okHttpClient.newCall(request).await().use { response ->
|
imageProxyInterceptor.interceptPageRequest(request, okHttpClient).use { response ->
|
||||||
check(response.isSuccessful) {
|
check(response.isSuccessful) {
|
||||||
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
"Invalid response: ${response.code} ${response.message} at $pageUrl"
|
||||||
}
|
}
|
||||||
@@ -89,11 +93,12 @@ class MangaPageFetcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Factory(
|
class Factory @Inject constructor(
|
||||||
private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val okHttpClient: OkHttpClient,
|
@MangaHttpClient private val okHttpClient: OkHttpClient,
|
||||||
private val pagesCache: PagesCache,
|
private val pagesCache: PagesCache,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
|
private val imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
) : Fetcher.Factory<MangaPage> {
|
) : Fetcher.Factory<MangaPage> {
|
||||||
|
|
||||||
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher {
|
override fun create(data: MangaPage, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||||
@@ -104,6 +109,7 @@ class MangaPageFetcher(
|
|||||||
page = data,
|
page = data,
|
||||||
context = context,
|
context = context,
|
||||||
mangaRepositoryFactory = mangaRepositoryFactory,
|
mangaRepositoryFactory = mangaRepositoryFactory,
|
||||||
|
imageProxyInterceptor = imageProxyInterceptor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,4 +426,6 @@
|
|||||||
<string name="invalid_value_message">Invalid value</string>
|
<string name="invalid_value_message">Invalid value</string>
|
||||||
<string name="manga_branch_title_template">%1$s (%2$s)</string>
|
<string name="manga_branch_title_template">%1$s (%2$s)</string>
|
||||||
<string name="downloaded">Downloaded</string>
|
<string name="downloaded">Downloaded</string>
|
||||||
|
<string name="images_proxy_title">Images optimization proxy</string>
|
||||||
|
<string name="images_procy_description">Use the wsrv.nl service to reduce traffic usage and speed up image loading if possible</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -37,6 +37,12 @@
|
|||||||
app:allowDividerAbove="true"
|
app:allowDividerAbove="true"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="images_proxy"
|
||||||
|
android:summary="@string/images_procy_description"
|
||||||
|
android:title="@string/images_proxy_title" />
|
||||||
|
|
||||||
<PreferenceScreen
|
<PreferenceScreen
|
||||||
android:fragment="org.koitharu.kotatsu.settings.ProxySettingsFragment"
|
android:fragment="org.koitharu.kotatsu.settings.ProxySettingsFragment"
|
||||||
android:key="proxy"
|
android:key="proxy"
|
||||||
|
|||||||
Reference in New Issue
Block a user