mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-29 04:27:51 +02:00
@ -39,7 +39,9 @@ class NetworkHelper(
|
||||
builder.addNetworkInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
builder.addInterceptor(CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider))
|
||||
builder.addInterceptor(
|
||||
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
|
||||
)
|
||||
|
||||
when (preferences.dohProvider().get()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
|
@ -17,6 +17,9 @@ class NetworkPreferences(
|
||||
}
|
||||
|
||||
fun defaultUserAgent(): Preference<String> {
|
||||
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0")
|
||||
return preferenceStore.getString(
|
||||
"default_user_agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,15 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||
@ -95,6 +104,9 @@ suspend fun Call.await(): Response {
|
||||
return await(callStack)
|
||||
}
|
||||
|
||||
/**
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
suspend fun Call.awaitSuccess(): Response {
|
||||
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
||||
val response = await(callStack)
|
||||
@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response {
|
||||
return response
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
|
@ -9,7 +9,10 @@ import okio.Source
|
||||
import okio.buffer
|
||||
import java.io.IOException
|
||||
|
||||
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
|
||||
class ProgressResponseBody(
|
||||
private val responseBody: ResponseBody,
|
||||
private val progressListener: ProgressListener,
|
||||
) : ResponseBody() {
|
||||
|
||||
private val bufferedSource: BufferedSource by lazy {
|
||||
source(responseBody.source()).buffer()
|
||||
@ -36,7 +39,11 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
// read() returns the number of bytes read, or -1 if this source is exhausted.
|
||||
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
||||
progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
|
||||
progressListener.update(
|
||||
totalBytesRead,
|
||||
responseBody.contentLength(),
|
||||
bytesRead == -1L,
|
||||
)
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,11 @@ class CloudflareInterceptor(
|
||||
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
|
||||
override fun intercept(
|
||||
chain: Interceptor.Chain,
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Response {
|
||||
try {
|
||||
response.close()
|
||||
cookieManager.remove(request.url, COOKIE_NAMES, 0)
|
||||
|
@ -33,7 +33,9 @@ fun OkHttpClient.Builder.rateLimitHost(
|
||||
permits: Int,
|
||||
period: Long = 1,
|
||||
unit: TimeUnit = TimeUnit.SECONDS,
|
||||
) = addInterceptor(RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())))
|
||||
) = addInterceptor(
|
||||
RateLimitInterceptor(httpUrl.host, permits, period.toDuration(unit.toDurationUnit())),
|
||||
)
|
||||
|
||||
/**
|
||||
* An OkHttp interceptor that handles given url host's rate limiting.
|
||||
@ -69,8 +71,5 @@ fun OkHttpClient.Builder.rateLimitHost(
|
||||
* @param permits [Int] Number of requests allowed within a period of units.
|
||||
* @param period [Duration] The limiting duration. Defaults to 1.seconds.
|
||||
*/
|
||||
fun OkHttpClient.Builder.rateLimitHost(
|
||||
url: String,
|
||||
permits: Int,
|
||||
period: Duration = 1.seconds,
|
||||
) = addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
|
||||
fun OkHttpClient.Builder.rateLimitHost(url: String, permits: Int, period: Duration = 1.seconds) =
|
||||
addInterceptor(RateLimitInterceptor(url.toHttpUrlOrNull()?.host, permits, period))
|
||||
|
@ -10,7 +10,11 @@ import androidx.annotation.StringRes
|
||||
* @param resource the text resource.
|
||||
* @param duration the duration of the toast. Defaults to short.
|
||||
*/
|
||||
fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
|
||||
fun Context.toast(
|
||||
@StringRes resource: Int,
|
||||
duration: Int = Toast.LENGTH_SHORT,
|
||||
block: (Toast) -> Unit = {},
|
||||
): Toast {
|
||||
return toast(getString(resource), duration, block)
|
||||
}
|
||||
|
||||
@ -20,7 +24,11 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT,
|
||||
* @param text the text to display.
|
||||
* @param duration the duration of the toast. Defaults to short.
|
||||
*/
|
||||
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
|
||||
fun Context.toast(
|
||||
text: String?,
|
||||
duration: Int = Toast.LENGTH_SHORT,
|
||||
block: (Toast) -> Unit = {},
|
||||
): Toast {
|
||||
return Toast.makeText(applicationContext, text.orEmpty(), duration).also {
|
||||
block(it)
|
||||
it.show()
|
||||
|
@ -47,10 +47,7 @@ abstract class WebViewClientCompat : WebViewClient() {
|
||||
return shouldInterceptRequestCompat(view, request.url.toString())
|
||||
}
|
||||
|
||||
final override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
url: String,
|
||||
): WebResourceResponse? {
|
||||
final override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
|
||||
return shouldInterceptRequestCompat(view, url)
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,24 @@ import kotlin.coroutines.resume
|
||||
object WebViewUtil {
|
||||
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
||||
|
||||
const val MINIMUM_WEBVIEW_VERSION = 111
|
||||
const val MINIMUM_WEBVIEW_VERSION = 114
|
||||
|
||||
/**
|
||||
* Uses the WebView's user agent string to create something similar to what Chrome on Android
|
||||
* would return.
|
||||
*
|
||||
* Example of WebView user agent string:
|
||||
* Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36
|
||||
*
|
||||
* Example of Chrome on Android:
|
||||
* Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.3
|
||||
*/
|
||||
fun getInferredUserAgent(context: Context): String {
|
||||
return WebView(context)
|
||||
.getDefaultUserAgentString()
|
||||
.replace("; Android .*?\\)".toRegex(), "; Android 10; K)")
|
||||
.replace("Version/.* Chrome/".toRegex(), "Chrome/")
|
||||
}
|
||||
|
||||
fun supportsWebView(context: Context): Boolean {
|
||||
try {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package tachiyomi.core
|
||||
|
||||
object Constants {
|
||||
const val URL_HELP = "https://tachiyomi.org/help/"
|
||||
const val URL_HELP = "https://tachiyomi.org/docs/guides/troubleshooting/"
|
||||
|
||||
const val MANGA_EXTRA = "manga"
|
||||
|
||||
|
@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import tachiyomi.core.util.system.logcat
|
||||
|
||||
sealed class AndroidPreference<T>(
|
||||
private val preferences: SharedPreferences,
|
||||
@ -29,7 +30,13 @@ sealed class AndroidPreference<T>(
|
||||
}
|
||||
|
||||
override fun get(): T {
|
||||
return read(preferences, key, defaultValue)
|
||||
return try {
|
||||
read(preferences, key, defaultValue)
|
||||
} catch (e: ClassCastException) {
|
||||
logcat { "Invalid value for $key; deleting" }
|
||||
delete()
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
override fun set(value: T) {
|
||||
@ -68,7 +75,11 @@ sealed class AndroidPreference<T>(
|
||||
key: String,
|
||||
defaultValue: String,
|
||||
) : AndroidPreference<String>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String {
|
||||
override fun read(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: String,
|
||||
): String {
|
||||
return preferences.getString(key, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
@ -128,7 +139,11 @@ sealed class AndroidPreference<T>(
|
||||
key: String,
|
||||
defaultValue: Boolean,
|
||||
) : AndroidPreference<Boolean>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean {
|
||||
override fun read(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: Boolean,
|
||||
): Boolean {
|
||||
return preferences.getBoolean(key, defaultValue)
|
||||
}
|
||||
|
||||
@ -143,7 +158,11 @@ sealed class AndroidPreference<T>(
|
||||
key: String,
|
||||
defaultValue: Set<String>,
|
||||
) : AndroidPreference<Set<String>>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Set<String>): Set<String> {
|
||||
override fun read(
|
||||
preferences: SharedPreferences,
|
||||
key: String,
|
||||
defaultValue: Set<String>,
|
||||
): Set<String> {
|
||||
return preferences.getStringSet(key, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
|
@ -15,10 +15,9 @@ import tachiyomi.core.preference.AndroidPreference.StringSetPrimitive
|
||||
|
||||
class AndroidPreferenceStore(
|
||||
context: Context,
|
||||
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context),
|
||||
) : PreferenceStore {
|
||||
|
||||
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
private val keyFlow = sharedPreferences.keyFlow
|
||||
|
||||
override fun getString(key: String, defaultValue: String): Preference<String> {
|
||||
@ -60,11 +59,19 @@ class AndroidPreferenceStore(
|
||||
deserializer = deserializer,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getAll(): Map<String, *> {
|
||||
return sharedPreferences.all ?: emptyMap<String, Any>()
|
||||
}
|
||||
}
|
||||
|
||||
private val SharedPreferences.keyFlow
|
||||
get() = callbackFlow {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) }
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? ->
|
||||
trySend(
|
||||
key,
|
||||
)
|
||||
}
|
||||
registerOnSharedPreferenceChangeListener(listener)
|
||||
awaitClose {
|
||||
unregisterOnSharedPreferenceChangeListener(listener)
|
||||
|
@ -21,9 +21,29 @@ interface Preference<T> {
|
||||
fun changes(): Flow<T>
|
||||
|
||||
fun stateIn(scope: CoroutineScope): StateFlow<T>
|
||||
|
||||
val isPrivate: Boolean
|
||||
get() = key().startsWith(PRIVATE_PREFIX)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A preference that should not be exposed in places like backups.
|
||||
*/
|
||||
fun isPrivate(key: String): Boolean {
|
||||
return key.startsWith(PRIVATE_PREFIX)
|
||||
}
|
||||
|
||||
fun privateKey(key: String): String {
|
||||
return "${PRIVATE_PREFIX}$key"
|
||||
}
|
||||
|
||||
private const val PRIVATE_PREFIX = "__PRIVATE_"
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(block(get()))
|
||||
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(
|
||||
block(get()),
|
||||
)
|
||||
|
||||
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
|
||||
set(get() + item)
|
||||
|
@ -31,6 +31,8 @@ interface PreferenceStore {
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T,
|
||||
): Preference<T>
|
||||
|
||||
fun getAll(): Map<String, *>
|
||||
}
|
||||
|
||||
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
||||
|
@ -52,9 +52,15 @@ fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launchIO { withContext(NonCancellable, block) }
|
||||
|
||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(
|
||||
Dispatchers.Main,
|
||||
block,
|
||||
)
|
||||
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
||||
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(
|
||||
Dispatchers.IO,
|
||||
block,
|
||||
)
|
||||
|
||||
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
|
||||
withContext(NonCancellable, block)
|
||||
|
@ -273,48 +273,6 @@ object ImageUtil {
|
||||
|
||||
private fun splitImageName(filenamePrefix: String, index: Int) = "${filenamePrefix}__${"%03d".format(index + 1)}.jpg"
|
||||
|
||||
/**
|
||||
* Check whether the image is a long Strip that needs splitting
|
||||
* @return true if the image is not animated and it's height is greater than image width and screen height
|
||||
*/
|
||||
fun isStripSplitNeeded(imageStream: BufferedInputStream): Boolean {
|
||||
if (isAnimatedAndSupported(imageStream)) return false
|
||||
|
||||
val options = extractImageOptions(imageStream)
|
||||
val imageHeightIsBiggerThanWidth = options.outHeight > options.outWidth
|
||||
val imageHeightBiggerThanScreenHeight = options.outHeight > optimalImageHeight
|
||||
return imageHeightIsBiggerThanWidth && imageHeightBiggerThanScreenHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the imageStream according to the provided splitData
|
||||
*/
|
||||
fun splitStrip(splitData: SplitData, streamFn: () -> InputStream): InputStream {
|
||||
val bitmapRegionDecoder = getBitmapRegionDecoder(streamFn())
|
||||
?: throw Exception("Failed to create new instance of BitmapRegionDecoder")
|
||||
|
||||
logcat {
|
||||
"WebtoonSplit #${splitData.index} with topOffset=${splitData.topOffset} " +
|
||||
"splitHeight=${splitData.splitHeight} bottomOffset=${splitData.bottomOffset}"
|
||||
}
|
||||
|
||||
try {
|
||||
val region = Rect(0, splitData.topOffset, splitData.splitWidth, splitData.bottomOffset)
|
||||
val splitBitmap = bitmapRegionDecoder.decodeRegion(region, null)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||
return ByteArrayInputStream(outputStream.toByteArray())
|
||||
} catch (e: Throwable) {
|
||||
throw e
|
||||
} finally {
|
||||
bitmapRegionDecoder.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
fun getSplitDataForStream(imageStream: InputStream): List<SplitData> {
|
||||
return extractImageOptions(imageStream).splitData
|
||||
}
|
||||
|
||||
private val BitmapFactory.Options.splitData
|
||||
get(): List<SplitData> {
|
||||
val imageHeight = outHeight
|
||||
|
Reference in New Issue
Block a user