revert: Revert "Revert "Modularize the app""

This reverts commit f59f2346dc.
This commit is contained in:
Ahmad Ansori Palembani 2024-06-16 18:39:16 +07:00
parent 9feba40ab7
commit 2b46f94a5a
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
290 changed files with 1655 additions and 881 deletions

View file

@ -39,14 +39,9 @@ val buildTime: String by lazy {
val supportedAbis = setOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
android {
compileSdk = AndroidConfig.compileSdk
ndkVersion = AndroidConfig.ndk
defaultConfig {
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
applicationId = "eu.kanade.tachiyomi"
versionCode = 137
versionCode = 138
versionName = "1.8.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled = true
@ -145,14 +140,6 @@ android {
kotlinCompilerExtensionVersion = compose.versions.compose.compiler.get()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "17"
}
namespace = "eu.kanade.tachiyomi"
sqldelight {
@ -167,6 +154,10 @@ android {
}
dependencies {
implementation(projects.core)
implementation(projects.i18n)
implementation(projects.sourceApi)
// Compose
implementation(compose.bundles.compose)
debugImplementation(compose.ui.tooling)
@ -280,8 +271,8 @@ dependencies {
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
implementation(kotlinx.coroutines.core)
implementation(kotlinx.coroutines.android)
implementation(platform(kotlinx.coroutines.bom))
implementation(kotlinx.bundles.coroutines)
// Text distance
implementation(libs.java.string.similarity)
@ -296,8 +287,6 @@ dependencies {
implementation(kotlinx.immutable)
"coreLibraryDesugaring"(libs.desugar)
// Tests
testImplementation(libs.bundles.test)
testRuntimeOnly(libs.bundles.test.runtime)
@ -306,13 +295,6 @@ dependencies {
}
tasks {
withType<Test> {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(

View file

@ -1,19 +1,19 @@
-dontobfuscate
-keep class eu.kanade.tachiyomi.source.** { public protected *; } # Avoid access modification
-keep,allowoptimization class eu.kanade.** { public protected *; }
-keep,allowoptimization class tachiyomi.** { public protected *; }
-keep,allowoptimization class dev.yokai.** { public protected *; }
-keep,allowoptimization class eu.kanade.**
-keep,allowoptimization class tachiyomi.**
-keep,allowoptimization class yokai.**
# Keep common dependencies used in extensions
-keep class androidx.preference.** { public protected *; }
-keep class kotlin.** { public protected *; }
-keep,allowoptimization class androidx.preference.** { public protected *; }
-keep,allowoptimization class kotlin.** { public protected *; }
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
-keep class kotlinx.serialization.** { public protected *; }
-keep class okhttp3.** { public protected *; }
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
-keep,allowoptimization class kotlin.time.** { public protected *; }
-keep,allowoptimization class okhttp3.** { public protected *; }
-keep,allowoptimization class okio.** { public protected *; }
-keep,allowoptimization class rx.** { public protected *; }
-keep class org.jsoup.** { public protected *; }
-keep,allowoptimization class org.jsoup.** { public protected *; }
-keep,allowoptimization class com.google.gson.** { public protected *; }
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
@ -73,11 +73,11 @@
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class dev.yokai.**$$serializer { *; }
-keepclassmembers class dev.yokai.** {
-keep,includedescriptorclasses class yokai.**$$serializer { *; }
-keepclassmembers class yokai.** {
*** Companion;
}
-keepclasseswithmembers class dev.yokai.** {
-keepclasseswithmembers class yokai.** {
kotlinx.serialization.KSerializer serializer(...);
}

View file

@ -1,56 +0,0 @@
package dev.yokai.core.di
import dev.yokai.data.chapter.ChapterRepositoryImpl
import dev.yokai.domain.extension.repo.ExtensionRepoRepository
import dev.yokai.data.extension.repo.ExtensionRepoRepositoryImpl
import dev.yokai.data.library.custom.CustomMangaRepositoryImpl
import dev.yokai.data.manga.MangaRepositoryImpl
import dev.yokai.domain.chapter.ChapterRepository
import dev.yokai.domain.chapter.interactor.GetAvailableScanlators
import dev.yokai.domain.chapter.interactor.GetChapters
import dev.yokai.domain.extension.interactor.TrustExtension
import dev.yokai.domain.extension.repo.interactor.CreateExtensionRepo
import dev.yokai.domain.extension.repo.interactor.DeleteExtensionRepo
import dev.yokai.domain.extension.repo.interactor.GetExtensionRepo
import dev.yokai.domain.extension.repo.interactor.GetExtensionRepoCount
import dev.yokai.domain.extension.repo.interactor.ReplaceExtensionRepo
import dev.yokai.domain.extension.repo.interactor.UpdateExtensionRepo
import dev.yokai.domain.library.custom.CustomMangaRepository
import dev.yokai.domain.library.custom.interactor.CreateCustomManga
import dev.yokai.domain.library.custom.interactor.DeleteCustomManga
import dev.yokai.domain.library.custom.interactor.GetCustomManga
import dev.yokai.domain.library.custom.interactor.RelinkCustomManga
import dev.yokai.domain.manga.MangaRepository
import dev.yokai.domain.manga.interactor.GetLibraryManga
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class DomainModule : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addFactory { TrustExtension(get(), get()) }
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
addFactory { CreateExtensionRepo(get()) }
addFactory { DeleteExtensionRepo(get()) }
addFactory { GetExtensionRepo(get()) }
addFactory { GetExtensionRepoCount(get()) }
addFactory { ReplaceExtensionRepo(get()) }
addFactory { UpdateExtensionRepo(get(), get()) }
addSingletonFactory<CustomMangaRepository> { CustomMangaRepositoryImpl(get()) }
addFactory { CreateCustomManga(get()) }
addFactory { DeleteCustomManga(get()) }
addFactory { GetCustomManga(get()) }
addFactory { RelinkCustomManga(get()) }
addSingletonFactory<MangaRepository> { MangaRepositoryImpl(get()) }
addFactory { GetLibraryManga(get()) }
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
addFactory { GetAvailableScanlators(get()) }
addFactory { GetChapters(get()) }
}
}

View file

@ -34,13 +34,6 @@ import coil3.request.crossfade
import coil3.util.DebugLogger
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import dev.yokai.core.CrashlyticsLogWriter
import dev.yokai.core.di.AppModule
import dev.yokai.core.di.DomainModule
import dev.yokai.core.di.PreferenceModule
import dev.yokai.core.migration.Migrator
import dev.yokai.core.migration.migrations.migrations
import dev.yokai.domain.base.BasePreferences
import eu.kanade.tachiyomi.appwidget.TachiyomiWidgetManager
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.core.preference.PreferenceStore
@ -67,6 +60,13 @@ import org.conscrypt.Conscrypt
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.core.CrashlyticsLogWriter
import yokai.core.di.AppModule
import yokai.core.di.DomainModule
import yokai.core.di.PreferenceModule
import yokai.core.migration.Migrator
import yokai.core.migration.migrations.migrations
import yokai.domain.base.BasePreferences
import java.security.Security
open class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factory {

View file

@ -1,194 +0,0 @@
package eu.kanade.tachiyomi.core.preference
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import androidx.core.content.edit
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
sealed class AndroidPreference<T>(
private val preferences: SharedPreferences,
private val keyFlow: Flow<String?>,
private val key: String,
private val defaultValue: T,
) : Preference<T> {
abstract fun read(preferences: SharedPreferences, key: String, defaultValue: T): T
abstract fun write(key: String, value: T): Editor.() -> Unit
override fun key(): String {
return key
}
override fun get(): T {
return try {
read(preferences, key, defaultValue)
} catch (e: ClassCastException) {
Logger.d { "Invalid value for $key; deleting" }
delete()
defaultValue
}
}
override fun set(value: T) {
preferences.edit(action = write(key, value))
}
override fun isSet(): Boolean {
return preferences.contains(key)
}
override fun delete() {
preferences.edit {
remove(key)
}
}
override fun defaultValue(): T {
return defaultValue
}
override fun changes(): Flow<T> {
return keyFlow
.filter { it == key || it == null }
.onStart { emit("ignition") }
.map { get() }
.conflate()
}
override fun stateIn(scope: CoroutineScope): StateFlow<T> {
return changes().stateIn(scope, SharingStarted.Eagerly, get())
}
class StringPrimitive(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: String,
) : AndroidPreference<String>(preferences, keyFlow, key, defaultValue) {
override fun read(
preferences: SharedPreferences,
key: String,
defaultValue: String,
): String {
return preferences.getString(key, defaultValue) ?: defaultValue
}
override fun write(key: String, value: String): Editor.() -> Unit = {
putString(key, value)
}
}
class LongPrimitive(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: Long,
) : AndroidPreference<Long>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long {
return preferences.getLong(key, defaultValue)
}
override fun write(key: String, value: Long): Editor.() -> Unit = {
putLong(key, value)
}
}
class IntPrimitive(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: Int,
) : AndroidPreference<Int>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int {
return preferences.getInt(key, defaultValue)
}
override fun write(key: String, value: Int): Editor.() -> Unit = {
putInt(key, value)
}
}
class FloatPrimitive(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: Float,
) : AndroidPreference<Float>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float {
return preferences.getFloat(key, defaultValue)
}
override fun write(key: String, value: Float): Editor.() -> Unit = {
putFloat(key, value)
}
}
class BooleanPrimitive(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: Boolean,
) : AndroidPreference<Boolean>(preferences, keyFlow, key, defaultValue) {
override fun read(
preferences: SharedPreferences,
key: String,
defaultValue: Boolean,
): Boolean {
return preferences.getBoolean(key, defaultValue)
}
override fun write(key: String, value: Boolean): Editor.() -> Unit = {
putBoolean(key, value)
}
}
class StringSetPrimitive(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: Set<String>,
) : AndroidPreference<Set<String>>(preferences, keyFlow, key, defaultValue) {
override fun read(
preferences: SharedPreferences,
key: String,
defaultValue: Set<String>,
): Set<String> {
return preferences.getStringSet(key, defaultValue) ?: defaultValue
}
override fun write(key: String, value: Set<String>): Editor.() -> Unit = {
putStringSet(key, value)
}
}
class Object<T>(
preferences: SharedPreferences,
keyFlow: Flow<String?>,
key: String,
defaultValue: T,
val serializer: (T) -> String,
val deserializer: (String) -> T,
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
return try {
preferences.getString(key, null)?.let(deserializer) ?: defaultValue
} catch (e: Exception) {
defaultValue
}
}
override fun write(key: String, value: T): Editor.() -> Unit = {
putString(key, serializer(value))
}
}
}

View file

@ -1,79 +0,0 @@
package eu.kanade.tachiyomi.core.preference
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.core.preference.AndroidPreference.BooleanPrimitive
import eu.kanade.tachiyomi.core.preference.AndroidPreference.FloatPrimitive
import eu.kanade.tachiyomi.core.preference.AndroidPreference.IntPrimitive
import eu.kanade.tachiyomi.core.preference.AndroidPreference.LongPrimitive
import eu.kanade.tachiyomi.core.preference.AndroidPreference.Object
import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringPrimitive
import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringSetPrimitive
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
class AndroidPreferenceStore(
context: Context,
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context),
) : PreferenceStore {
private val keyFlow = sharedPreferences.keyFlow
override fun getString(key: String, defaultValue: String): Preference<String> {
return StringPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
return LongPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
return IntPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
return FloatPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
return BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
}
override fun <T> getObject(
key: String,
defaultValue: T,
serializer: (T) -> String,
deserializer: (String) -> T,
): Preference<T> {
return Object(
preferences = sharedPreferences,
keyFlow = keyFlow,
key = key,
defaultValue = defaultValue,
serializer = serializer,
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,
)
}
registerOnSharedPreferenceChangeListener(listener)
awaitClose {
unregisterOnSharedPreferenceChangeListener(listener)
}
}

View file

@ -1,85 +0,0 @@
package eu.kanade.tachiyomi.core.preference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface Preference<T> {
fun key(): String
fun get(): T
fun set(value: T)
fun isSet(): Boolean
fun delete()
fun defaultValue(): T
fun changes(): Flow<T>
fun stateIn(scope: CoroutineScope): StateFlow<T>
companion object {
/**
* A preference that should not be exposed in places like backups without user consent.
*/
fun isPrivate(key: String): Boolean {
return key.startsWith(PRIVATE_PREFIX)
}
fun privateKey(key: String): String {
return "${PRIVATE_PREFIX}$key"
}
/**
* A preference used for internal app state that isn't really a user preference
* and therefore should not be in places like backups.
*/
fun isAppState(key: String): Boolean {
return key.startsWith(APP_STATE_PREFIX)
}
fun appStateKey(key: String): String {
return "${APP_STATE_PREFIX}$key"
}
private const val APP_STATE_PREFIX = "__APP_STATE_"
private const val PRIVATE_PREFIX = "__PRIVATE_"
}
}
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(
block(get()),
)
operator fun <T> Preference<Set<T>>.plusAssign(item: Collection<T>) {
get() + item
}
operator fun <T> Preference<Set<T>>.minusAssign(item: Collection<T>) {
get() - item
}
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
set(get() + item)
}
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item)
}
fun Preference<Boolean>.toggle(): Boolean {
set(!get())
return get()
}
@Composable
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
return flow.collectAsState(initial = get())
}

View file

@ -1,43 +0,0 @@
package eu.kanade.tachiyomi.core.preference
interface PreferenceStore {
fun getString(key: String, defaultValue: String = ""): Preference<String>
fun getLong(key: String, defaultValue: Long = 0): Preference<Long>
fun getInt(key: String, defaultValue: Int = 0): Preference<Int>
fun getFloat(key: String, defaultValue: Float = 0f): Preference<Float>
fun getBoolean(key: String, defaultValue: Boolean = false): Preference<Boolean>
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
fun <T> getObject(
key: String,
defaultValue: T,
serializer: (T) -> String,
deserializer: (String) -> T,
): Preference<T>
fun getAll(): Map<String, *>
}
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
key: String,
defaultValue: T,
): Preference<T> {
return getObject(
key = key,
defaultValue = defaultValue,
serializer = { it.name },
deserializer = {
try {
enumValueOf(it)
} catch (e: IllegalArgumentException) {
defaultValue
}
},
)
}

View file

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.core.storage.preference
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import eu.kanade.tachiyomi.core.preference.Preference
@Composable
fun <T> Preference<T>.collectAsState(): State<T> {
val flow = remember(this) { changes() }
return flow.collectAsState(initial = get())
}

View file

@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
@ -22,6 +21,7 @@ import okio.sink
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.storage.StorageManager
import java.io.FileOutputStream
class BackupCreator(

View file

@ -14,7 +14,6 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -24,6 +23,7 @@ import eu.kanade.tachiyomi.util.system.notificationManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.storage.StorageManager
import java.util.concurrent.*
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :

View file

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.data.backup.models
import dev.yokai.core.metadata.ComicInfo
import dev.yokai.core.metadata.ComicInfoPublishingStatus
import dev.yokai.domain.library.custom.model.CustomMangaInfo
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
@ -12,6 +9,7 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.util.chapter.ChapterUtil
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import yokai.domain.library.custom.model.CustomMangaInfo
@Suppress("DEPRECATION")
@Serializable

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.backup.restore.restorers
import dev.yokai.domain.library.custom.model.CustomMangaInfo
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
@ -18,6 +17,7 @@ import eu.kanade.tachiyomi.util.manga.MangaUtil
import eu.kanade.tachiyomi.util.system.launchNow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.library.custom.model.CustomMangaInfo
import kotlin.math.max
class MangaBackupRestorer(

View file

@ -1,5 +1,18 @@
package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SChapter
fun SChapter.toChapter(): ChapterImpl {
return ChapterImpl().apply {
name = this@SChapter.name
url = this@SChapter.url
date_upload = this@SChapter.date_upload
chapter_number = this@SChapter.chapter_number
scanlator = this@SChapter.scanlator
}
}
class ChapterImpl : Chapter {
override var id: Long? = null

View file

@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
import java.util.*
interface Manga : SManga {
@ -34,6 +34,55 @@ interface Manga : SManga {
var filtered_scanlators: String?
val originalTitle: String
get() = (this as? MangaImpl)?.ogTitle ?: title
val originalAuthor: String?
get() = (this as? MangaImpl)?.ogAuthor ?: author
val originalArtist: String?
get() = (this as? MangaImpl)?.ogArtist ?: artist
val originalDescription: String?
get() = (this as? MangaImpl)?.ogDesc ?: description
val originalGenre: String?
get() = (this as? MangaImpl)?.ogGenre ?: genre
val originalStatus: Int
get() = (this as? MangaImpl)?.ogStatus ?: status
val hasSameAuthorAndArtist: Boolean
get() = author == artist || artist.isNullOrBlank() ||
author?.contains(artist ?: "", true) == true
fun copyFrom(other: SManga) {
if (other is Manga) {
if (other.author != null) {
author = other.originalAuthor
}
if (other.artist != null) {
artist = other.originalArtist
}
if (other.description != null) {
description = other.originalDescription
}
if (other.genre != null) {
genre = other.originalGenre
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.originalStatus
}
update_strategy = other.update_strategy
if (!initialized) {
initialized = other.initialized
}
}
fun isBlank() = id == Long.MIN_VALUE
fun isHidden() = status == -1

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import com.hippo.unifile.UniFile
import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@ -16,7 +15,8 @@ import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
import yokai.domain.storage.StorageManager
import java.util.concurrent.*
/**
* Cache where we dump the downloads directory from the filesystem. This class is needed because

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import dev.yokai.domain.download.DownloadPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@ -18,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy
import yokai.domain.download.DownloadPreferences
/**
* This class is used to manage chapter downloads in the application. It must be instantiated once

View file

@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import com.hippo.unifile.UniFile
import dev.yokai.domain.download.DownloadPreferences
import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
@ -17,6 +15,8 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
import yokai.domain.download.DownloadPreferences
import yokai.domain.storage.StorageManager
/**
* This class is used to provide the directories where the downloads should be saved.

View file

@ -6,10 +6,6 @@ import android.os.Looper
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.PublishRelay
import dev.yokai.core.metadata.COMIC_INFO_FILE
import dev.yokai.core.metadata.ComicInfo
import dev.yokai.core.metadata.getComicInfo
import dev.yokai.domain.download.DownloadPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -52,11 +48,13 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
import yokai.core.metadata.getComicInfo
import yokai.domain.download.DownloadPreferences
import java.io.BufferedOutputStream
import java.io.File
import java.util.zip.CRC32
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import java.util.zip.*
/**
* This class is the one in charge of downloading chapters.

View file

@ -2,16 +2,6 @@ package eu.kanade.tachiyomi.data.library
import android.content.Context
import com.hippo.unifile.UniFile
import dev.yokai.core.metadata.COMIC_INFO_EDITS_FILE
import dev.yokai.core.metadata.ComicInfo
import dev.yokai.core.metadata.ComicInfoPublishingStatus
import dev.yokai.core.metadata.copyFromComicInfo
import dev.yokai.domain.library.custom.interactor.CreateCustomManga
import dev.yokai.domain.library.custom.interactor.DeleteCustomManga
import dev.yokai.domain.library.custom.interactor.GetCustomManga
import dev.yokai.domain.library.custom.interactor.RelinkCustomManga
import dev.yokai.domain.library.custom.model.CustomMangaInfo
import dev.yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import kotlinx.coroutines.CoroutineScope
@ -26,6 +16,16 @@ import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import nl.adaptivity.xmlutil.serialization.XmlValue
import uy.kohesive.injekt.injectLazy
import yokai.core.metadata.COMIC_INFO_EDITS_FILE
import yokai.core.metadata.ComicInfo
import yokai.core.metadata.ComicInfoPublishingStatus
import yokai.core.metadata.copyFromComicInfo
import yokai.domain.library.custom.interactor.CreateCustomManga
import yokai.domain.library.custom.interactor.DeleteCustomManga
import yokai.domain.library.custom.interactor.GetCustomManga
import yokai.domain.library.custom.interactor.RelinkCustomManga
import yokai.domain.library.custom.model.CustomMangaInfo
import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
import java.nio.charset.StandardCharsets
class CustomMangaManager(val context: Context) {

View file

@ -20,7 +20,6 @@ import co.touchlab.kermit.Logger
import coil3.imageLoader
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import dev.yokai.domain.manga.interactor.GetLibraryManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -75,12 +74,12 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.manga.interactor.GetLibraryManga
import java.io.File
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.*
import java.util.concurrent.atomic.*
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {

View file

@ -11,7 +11,6 @@ import eu.kanade.tachiyomi.core.preference.getEnum
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.ui.library.LibraryItem
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
@ -401,10 +400,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
fun useLargeToolbar() = preferenceStore.getBoolean("use_large_toolbar", true)
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
fun defaultUserAgent() = preferenceStore.getString("default_user_agent", NetworkHelper.DEFAULT_USER_AGENT)
fun showSeriesInShortcuts() = prefs.getBoolean(Keys.showSeriesInShortcuts, true)
fun showSourcesInShortcuts() = prefs.getBoolean(Keys.showSourcesInShortcuts, true)
fun openChapterInShortcuts() = prefs.getBoolean(Keys.openChapterInShortcuts, true)

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.updater
import android.content.Context
import android.os.Build
import androidx.annotation.VisibleForTesting
import dev.yokai.domain.base.models.Version
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.GET
@ -15,6 +14,7 @@ import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.base.models.Version
import java.util.*
import java.util.concurrent.*

View file

@ -5,8 +5,6 @@ import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Parcelable
import co.touchlab.kermit.Logger
import dev.yokai.domain.base.BasePreferences
import dev.yokai.domain.extension.interactor.TrustExtension
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.model.Extension
@ -17,7 +15,6 @@ import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.e
import eu.kanade.tachiyomi.util.system.launchNow
import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.coroutines.CoroutineScope
@ -28,6 +25,8 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.parcelize.Parcelize
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.base.BasePreferences
import yokai.domain.extension.interactor.TrustExtension
import java.util.*
import java.util.concurrent.*
@ -155,7 +154,7 @@ class ExtensionManager(
val extensions: List<Extension.Available> = try {
api.findExtensions()
} catch (e: Exception) {
Logger.e(e)
Logger.e(e) { "Failed to find available extensions" }
emptyList()
}

View file

@ -18,8 +18,6 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import dev.yokai.domain.base.BasePreferences
import dev.yokai.domain.base.BasePreferences.ExtensionInstaller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -39,6 +37,8 @@ import rikka.shizuku.Shizuku
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import yokai.domain.base.BasePreferences.ExtensionInstaller
import java.util.concurrent.*
class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParameters) :

View file

@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.extension.api
import android.content.Context
import co.touchlab.kermit.Logger
import dev.yokai.domain.extension.repo.interactor.GetExtensionRepo
import dev.yokai.domain.extension.repo.interactor.UpdateExtensionRepo
import dev.yokai.domain.extension.repo.model.ExtensionRepo
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
@ -21,6 +18,9 @@ import kotlinx.serialization.json.Json
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.extension.repo.interactor.GetExtensionRepo
import yokai.domain.extension.repo.interactor.UpdateExtensionRepo
import yokai.domain.extension.repo.model.ExtensionRepo
internal class ExtensionApi {

View file

@ -12,7 +12,6 @@ import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import co.touchlab.kermit.Logger
import dev.yokai.domain.base.BasePreferences
import eu.kanade.tachiyomi.extension.ExtensionInstallerJob
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.ShizukuInstaller
@ -47,6 +46,7 @@ import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import java.io.File
/**

View file

@ -9,7 +9,6 @@ import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import co.touchlab.kermit.Logger
import dalvik.system.PathClassLoader
import dev.yokai.domain.extension.interactor.TrustExtension
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.Extension
@ -23,6 +22,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy
import yokai.domain.extension.interactor.TrustExtension
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.BasicFileAttributes

View file

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.network
import android.webkit.CookieManager
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
class AndroidCookieJar : CookieJar {
private val manager = CookieManager.getInstance()
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val urlString = url.toString()
cookies.forEach { manager.setCookie(urlString, it.toString()) }
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return get(url)
}
fun get(url: HttpUrl): List<Cookie> {
val cookies = manager.getCookie(url.toString())
return if (cookies != null && cookies.isNotEmpty()) {
cookies.split(";").mapNotNull { Cookie.parse(url, it) }
} else {
emptyList()
}
}
fun remove(url: HttpUrl, cookieNames: List<String>? = null, maxAge: Int = -1): Int {
val urlString = url.toString()
val cookies = manager.getCookie(urlString) ?: return 0
fun List<String>.filterNames(): List<String> {
return if (cookieNames != null) {
this.filter { it in cookieNames }
} else {
this
}
}
return cookies.split(";")
.map { it.substringBefore("=") }
.filterNames()
.onEach { manager.setCookie(urlString, "$it=;Max-Age=$maxAge") }
.count()
}
fun removeAll() {
manager.removeAllCookies {}
}
}

View file

@ -1,122 +0,0 @@
package eu.kanade.tachiyomi.network
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import java.net.InetAddress
/**
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
*/
const val PREF_DOH_CLOUDFLARE = 1
const val PREF_DOH_GOOGLE = 2
const val PREF_DOH_ADGUARD = 3
const val PREF_DOH_QUAD9 = 4
const val PREF_DOH_ALIDNS = 5
const val PREF_DOH_DNSPOD = 6
const val PREF_DOH_360 = 7
const val PREF_DOH_QUAD101 = 8
fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400"),
)
.build(),
)
fun OkHttpClient.Builder.dohGoogle() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.google/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("8.8.4.4"),
InetAddress.getByName("8.8.8.8"),
InetAddress.getByName("2001:4860:4860::8888"),
InetAddress.getByName("2001:4860:4860::8844"),
)
.build(),
)
// AdGuard "Default" DNS works too but for the sake of making sure no site is blacklisted,
// we use "Unfiltered"
fun OkHttpClient.Builder.dohAdGuard() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns-unfiltered.adguard.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("94.140.14.140"),
InetAddress.getByName("94.140.14.141"),
InetAddress.getByName("2a10:50c0::1:ff"),
InetAddress.getByName("2a10:50c0::2:ff"),
)
.build(),
)
fun OkHttpClient.Builder.dohQuad9() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.quad9.net/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("9.9.9.9"),
InetAddress.getByName("149.112.112.112"),
InetAddress.getByName("2620:fe::fe"),
InetAddress.getByName("2620:fe::9"),
)
.build(),
)
fun OkHttpClient.Builder.dohAliDNS() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.alidns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("223.5.5.5"),
InetAddress.getByName("223.6.6.6"),
InetAddress.getByName("2400:3200::1"),
InetAddress.getByName("2400:3200:baba::1"),
)
.build(),
)
fun OkHttpClient.Builder.dohDNSPod() = dns(
DnsOverHttps.Builder().client(build())
.url("https://doh.pub/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("1.12.12.12"),
InetAddress.getByName("120.53.53.53"),
)
.build(),
)
fun OkHttpClient.Builder.doh360() = dns(
DnsOverHttps.Builder().client(build())
.url("https://doh.360.cn/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("101.226.4.6"),
InetAddress.getByName("218.30.118.6"),
InetAddress.getByName("123.125.81.6"),
InetAddress.getByName("140.207.198.6"),
InetAddress.getByName("180.163.249.75"),
InetAddress.getByName("101.199.113.208"),
InetAddress.getByName("36.99.170.86"),
)
.build(),
)
fun OkHttpClient.Builder.dohQuad101() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.twnic.tw/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("101.101.101.101"),
InetAddress.getByName("2001:de4::101"),
InetAddress.getByName("2001:de4::102"),
)
.build(),
)

View file

@ -1,26 +0,0 @@
package eu.kanade.tachiyomi.network
import android.content.Context
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.util.system.withIOContext
/**
* Util for evaluating JavaScript in sources.
*/
class JavaScriptEngine(context: Context) {
/**
* Evaluate arbitrary JavaScript code and get the result as a primtive type
* (e.g., String, Int).
*
* @since extensions-lib 1.4
* @param script JavaScript to execute.
* @return Result of JavaScript code as a primitive type.
*/
@Suppress("UNUSED", "UNCHECKED_CAST")
suspend fun <T> evaluate(script: String): T = withIOContext {
QuickJs.create().use {
it.evaluate(script) as T
}
}
}

View file

@ -1,83 +0,0 @@
package eu.kanade.tachiyomi.network
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache
import okhttp3.OkHttpClient
import okhttp3.brotli.BrotliInterceptor
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.TimeUnit
class NetworkHelper(val context: Context) {
private val preferences: PreferencesHelper by injectLazy()
private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieJar = AndroidCookieJar()
private val userAgentInterceptor by lazy { UserAgentInterceptor(::defaultUserAgent) }
private val cloudflareInterceptor by lazy {
CloudflareInterceptor(context, cookieJar, ::defaultUserAgent)
}
private val baseClientBuilder: OkHttpClient.Builder
get() {
val builder = OkHttpClient.Builder()
.cookieJar(cookieJar)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(userAgentInterceptor)
.addNetworkInterceptor(IgnoreGzipInterceptor())
.addNetworkInterceptor(BrotliInterceptor)
.apply {
if (BuildConfig.DEBUG) {
addInterceptor(
ChuckerInterceptor.Builder(context)
.collector(ChuckerCollector(context))
.maxContentLength(250000L)
.redactHeaders(emptySet())
.alwaysReadResponseBody(false)
.build(),
)
}
when (preferences.dohProvider()) {
PREF_DOH_CLOUDFLARE -> dohCloudflare()
PREF_DOH_GOOGLE -> dohGoogle()
PREF_DOH_ADGUARD -> dohAdGuard()
PREF_DOH_QUAD9 -> dohQuad9()
}
}
return builder
}
val client by lazy { baseClientBuilder.cache(Cache(cacheDir, cacheSize)).build() }
@Suppress("UNUSED")
val cloudflareClient by lazy {
client.newBuilder()
.addInterceptor(cloudflareInterceptor)
.build()
}
val defaultUserAgent
get() = preferences.defaultUserAgent().get().replace("\n", " ").trim()
companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0"
}
}

View file

@ -1,156 +0,0 @@
package eu.kanade.tachiyomi.network
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.okio.decodeFromBufferedSource
import kotlinx.serialization.serializer
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.coroutines.resumeWithException
val jsonMime = "application/json; charset=utf-8".toMediaType()
fun Call.asObservable(): Observable<Response> {
return Observable.unsafeCreate { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber.
val call = clone()
// Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
try {
val response = call.execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (e: Exception) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(e)
}
}
}
override fun unsubscribe() {
call.cancel()
}
override fun isUnsubscribed(): Boolean {
return call.isCanceled()
}
}
subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
}
}
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 {
return suspendCancellableCoroutine { continuation ->
val callback =
object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response) {
response.body.close()
}
}
override fun onFailure(call: Call, e: IOException) {
// Don't bother with resuming the continuation if it is already cancelled.
if (continuation.isCancelled) return
val exception = IOException(e.message, e).apply { stackTrace = callStack }
continuation.resumeWithException(exception)
}
}
enqueue(callback)
continuation.invokeOnCancellation {
try {
cancel()
} catch (ex: Throwable) {
// Ignore cancel exception
}
}
}
}
suspend fun Call.await(): Response {
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
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)
if (!response.isSuccessful) {
response.close()
throw HttpException(response.code).apply { stackTrace = callStack }
}
return response
}
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body, listener))
.build()
}
.build()
return progressClient.newCall(request)
}
context(Json)
inline fun <reified T> Response.parseAs(): T {
return decodeFromJsonResponse(serializer(), this)
}
context(Json)
fun <T> decodeFromJsonResponse(
deserializer: DeserializationStrategy<T>,
response: Response,
): T {
return response.body.source().use {
decodeFromBufferedSource(deserializer, it)
}
}
/**
* Exception that handles HTTP codes considered not successful by OkHttp.
* Use it to have a standardized error message in the app across the extensions.
*
* @since extensions-lib 1.5
* @param code [Int] the HTTP status code
*/
class HttpException(val code: Int) : IllegalStateException("HTTP error $code")

View file

@ -1,5 +0,0 @@
package eu.kanade.tachiyomi.network
interface ProgressListener {
fun update(bytesRead: Long, contentLength: Long, done: Boolean)
}

View file

@ -1,44 +0,0 @@
package eu.kanade.tachiyomi.network
import okhttp3.MediaType
import okhttp3.ResponseBody
import okio.Buffer
import okio.BufferedSource
import okio.ForwardingSource
import okio.Source
import okio.buffer
import java.io.IOException
class ProgressResponseBody(private val responseBody: ResponseBody, private val progressListener: ProgressListener) : ResponseBody() {
private val bufferedSource: BufferedSource by lazy {
source(responseBody.source()).buffer()
}
override fun contentType(): MediaType? {
return responseBody.contentType()
}
override fun contentLength(): Long {
return responseBody.contentLength()
}
override fun source(): BufferedSource {
return bufferedSource
}
private fun source(source: Source): Source {
return object : ForwardingSource(source) {
var totalBytesRead = 0L
@Throws(IOException::class)
override fun read(sink: Buffer, byteCount: Long): Long {
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)
return bytesRead
}
}
}
}

View file

@ -1,79 +0,0 @@
package eu.kanade.tachiyomi.network
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.RequestBody
import java.util.concurrent.TimeUnit.MINUTES
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
private val DEFAULT_HEADERS = Headers.Builder().build()
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
fun GET(
url: String,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return GET(url.toHttpUrl(), headers, cache)
}
/**
* @since extensions-lib 1.4
*/
fun GET(
url: HttpUrl,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
.headers(headers)
.cacheControl(cache)
.build()
}
fun POST(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
.post(body)
.headers(headers)
.cacheControl(cache)
.build()
}
fun PUT(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
.put(body)
.headers(headers)
.cacheControl(cache)
.build()
}
fun DELETE(
url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL,
): Request {
return Request.Builder()
.url(url)
.delete(body)
.headers(headers)
.cacheControl(cache)
.build()
}

View file

@ -1,145 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint
import android.content.Context
import android.webkit.WebView
import android.widget.Toast
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.AndroidCookieJar
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.CountDownLatch
class CloudflareInterceptor(
private val context: Context,
private val cookieManager: AndroidCookieJar,
defaultUserAgentProvider: () -> String,
) : WebViewInterceptor(context, defaultUserAgentProvider) {
private val executor = ContextCompat.getMainExecutor(context)
override fun shouldIntercept(response: Response): Boolean {
// Check if Cloudflare anti-bot is on
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
}
override fun intercept(
chain: Interceptor.Chain,
request: Request,
response: Response,
): Response {
try {
response.close()
cookieManager.remove(request.url, COOKIE_NAMES, 0)
val oldCookie = cookieManager.get(request.url)
.firstOrNull { it.name == "cf_clearance" }
resolveWithWebView(request, oldCookie)
return chain.proceed(request)
}
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app
catch (e: CloudflareBypassException) {
throw IOException(context.getString(R.string.failed_to_bypass_cloudflare))
} catch (e: Exception) {
throw IOException(e)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun resolveWithWebView(originalRequest: Request, oldCookie: Cookie?) {
// We need to lock this thread until the WebView finds the challenge solution url, because
// OkHttp doesn't support asynchronous interceptors.
val latch = CountDownLatch(1)
var webView: WebView? = null
var challengeFound = false
var cloudflareBypassed = false
var isWebViewOutdated = false
val origRequestUrl = originalRequest.url.toString()
val headers = parseHeaders(originalRequest.headers)
executor.execute {
webView = createWebView(originalRequest)
webView?.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) {
fun isCloudFlareBypassed(): Boolean {
return cookieManager.get(origRequestUrl.toHttpUrl())
.firstOrNull { it.name == "cf_clearance" }
.let { it != null && it != oldCookie }
}
if (isCloudFlareBypassed()) {
cloudflareBypassed = true
latch.countDown()
}
if (url == origRequestUrl && !challengeFound) {
// The first request didn't return the challenge, abort.
latch.countDown()
}
}
override fun onReceivedErrorCompat(
view: WebView,
errorCode: Int,
description: String?,
failingUrl: String,
isMainFrame: Boolean,
) {
if (isMainFrame) {
if (errorCode in ERROR_CODES) {
// Found the Cloudflare challenge page.
challengeFound = true
} else {
// Unlock thread, the challenge wasn't found.
latch.countDown()
}
}
}
}
webView?.loadUrl(origRequestUrl, headers)
}
latch.awaitFor30Seconds()
executor.execute {
if (!cloudflareBypassed) {
isWebViewOutdated = webView?.isOutdated() == true
}
webView?.run {
stopLoading()
destroy()
}
}
// Throw exception if we failed to bypass Cloudflare
if (!cloudflareBypassed) {
// Prompt user to update WebView if it seems too outdated
if (isWebViewOutdated) {
context.toast(R.string.please_update_webview, Toast.LENGTH_LONG)
}
throw CloudflareBypassException()
}
}
}
private val ERROR_CODES = listOf(403, 503)
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("cf_clearance")
private class CloudflareBypassException : Exception()

View file

@ -1,21 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
/**
* To use [okhttp3.brotli.BrotliInterceptor] as a network interceptor,
* add [IgnoreGzipInterceptor] right before it.
*
* This nullifies the transparent gzip of [okhttp3.internal.http.BridgeInterceptor]
* so gzip and Brotli are explicitly handled by the [okhttp3.brotli.BrotliInterceptor].
*/
class IgnoreGzipInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (request.header("Accept-Encoding") == "gzip") {
request = request.newBuilder().removeHeader("Accept-Encoding").build()
}
return chain.proceed(request)
}
}

View file

@ -1,78 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
* An OkHttp interceptor that handles rate limiting.
*
* Examples:
*
* permits = 5, period = 1, unit = seconds => 5 requests per second
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
*
* @since extension-lib 1.3
*
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(permits, period, unit))
private class RateLimitInterceptor(
private val permits: Int,
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
override fun intercept(chain: Interceptor.Chain): Response {
// Ignore canceled calls, otherwise they would jam the queue
if (chain.call().isCanceled()) {
throw IOException()
}
synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) {
0
} else {
val oldestReq = requestQueue[0]
val newestReq = requestQueue[permits - 1]
if (newestReq - oldestReq > rateLimitMillis) {
0
} else {
oldestReq + rateLimitMillis - now // Remaining time
}
}
// Final check
if (chain.call().isCanceled()) {
throw IOException()
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}
if (waitTime > 0) {
requestQueue.add(now + waitTime)
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
} else {
requestQueue.add(now)
}
}
return chain.proceed(chain.request())
}
}

View file

@ -1,85 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import android.os.SystemClock
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
* An OkHttp interceptor that handles given url host's rate limiting.
*
* Examples:
*
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.3
*
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.
*/
fun OkHttpClient.Builder.rateLimitHost(
httpUrl: HttpUrl,
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(SpecificHostRateLimitInterceptor(httpUrl, permits, period, unit))
class SpecificHostRateLimitInterceptor(
httpUrl: HttpUrl,
private val permits: Int,
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayList<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
private val host = httpUrl.host
override fun intercept(chain: Interceptor.Chain): Response {
// Ignore canceled calls, otherwise they would jam the queue
if (chain.call().isCanceled()) {
throw IOException()
} else if (chain.request().url.host != host) {
return chain.proceed(chain.request())
}
synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) {
0
} else {
val oldestReq = requestQueue[0]
val newestReq = requestQueue[permits - 1]
if (newestReq - oldestReq > rateLimitMillis) {
0
} else {
oldestReq + rateLimitMillis - now // Remaining time
}
}
// Final check
if (chain.call().isCanceled()) {
throw IOException()
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}
if (waitTime > 0) {
requestQueue.add(now + waitTime)
Thread.sleep(waitTime) // Sleep inside synchronized to pause queued requests
} else {
requestQueue.add(now)
}
}
return chain.proceed(chain.request())
}
}

View file

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
/**
* Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal
* IOException to avoid catastrophic failure.
*
* This should be the first interceptor in the client.
*
* See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/
*/
class UncaughtExceptionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: Exception) {
throw IOException(e)
}
}
}

View file

@ -1,24 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
class UserAgentInterceptor(
private val defaultUserAgentProvider: () -> String,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", defaultUserAgentProvider())
.build()
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
}
}

View file

@ -1,100 +0,0 @@
package eu.kanade.tachiyomi.network.interceptor
import android.content.Context
import android.os.Build
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.util.Locale
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
abstract class WebViewInterceptor(
private val context: Context,
private val defaultUserAgentProvider: () -> String,
) : Interceptor {
/**
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
* blocking the main thread too much. If used too often we could consider moving it to the
* Application class.
*/
private val initWebView by lazy {
// Crashes on some devices. We skip this in some cases since the only impact is slower
// WebView init in those rare cases.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
return@lazy
}
try {
WebSettings.getDefaultUserAgent(context)
} catch (_: Exception) {
// Avoid some crashes like when Chrome/WebView is being updated.
}
}
abstract fun shouldIntercept(response: Response): Boolean
abstract fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (!shouldIntercept(response)) {
return response
}
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
context.toast(R.string.webview_is_required, Toast.LENGTH_LONG)
}
return response
}
initWebView
return intercept(chain, request, response)
}
fun parseHeaders(headers: Headers): Map<String, String> {
return headers
// Keeping unsafe header makes webview throw [net::ERR_INVALID_ARGUMENT]
.filter { (name, value) ->
isRequestHeaderSafe(name, value)
}
.groupBy(keySelector = { (name, _) -> name }) { (_, value) -> value }
.mapValues { it.value.getOrNull(0).orEmpty() }
}
fun CountDownLatch.awaitFor30Seconds() {
await(30, TimeUnit.SECONDS)
}
fun createWebView(request: Request): WebView {
return WebView(context).apply {
setDefaultSettings()
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
settings.userAgentString = request.header("User-Agent") ?: defaultUserAgentProvider()
}
}
}
// Based on [IsRequestHeaderSafe] in https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc
private fun isRequestHeaderSafe(_name: String, _value: String): Boolean {
val name = _name.lowercase(Locale.ENGLISH)
val value = _value.lowercase(Locale.ENGLISH)
if (name in unsafeHeaderNames || name.startsWith("proxy-")) return false
if (name == "connection" && value == "upgrade") return false
return true
}
private val unsafeHeaderNames = listOf("content-length", "host", "trailer", "te", "upgrade", "cookie2", "keep-alive", "transfer-encoding", "set-cookie")

View file

@ -1,80 +0,0 @@
package eu.kanade.tachiyomi.source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.util.system.awaitSingle
import rx.Observable
interface CatalogueSource : Source {
/**
* An ISO 639-1 compliant language code (two letters in lower case).
*/
override val lang: String
/**
* Whether the source has support for latest updates.
*/
val supportsLatest: Boolean
/**
* Get a page with a list of manga.
*
* @since extensions-lib 1.5
* @param page the page number to retrieve.
*/
@Suppress("DEPRECATION")
suspend fun getPopularManga(page: Int): MangasPage {
return fetchPopularManga(page).awaitSingle()
}
/**
* Get a page with a list of manga.
*
* @since extensions-lib 1.5
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
@Suppress("DEPRECATION")
suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
return fetchSearchManga(page, query, filters).awaitSingle()
}
/**
* Get a page with a list of latest manga updates.
*
* @since extensions-lib 1.5
* @param page the page number to retrieve.
*/
@Suppress("DEPRECATION")
suspend fun getLatestUpdates(page: Int): MangasPage {
return fetchLatestUpdates(page).awaitSingle()
}
/**
* Returns the list of filters for the source.
*/
fun getFilterList(): FilterList
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPopularManga"),
)
fun fetchPopularManga(page: Int): Observable<MangasPage> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getSearchManga"),
)
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getLatestUpdates"),
)
fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
throw IllegalStateException("Not used")
}

View file

@ -1,28 +0,0 @@
package eu.kanade.tachiyomi.source
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
interface ConfigurableSource : Source {
/**
* Gets instance of [SharedPreferences] scoped to the specific source.
*
* @since extensions-lib 1.5
*/
fun getSourcePreferences(): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
fun setupPreferenceScreen(screen: PreferenceScreen)
}
// TODO: use getSourcePreferences once all extensions are on ext-lib 1.5
fun ConfigurableSource.sourcePreferences(): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
fun sourcePreferences(key: String): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(key, Context.MODE_PRIVATE)

View file

@ -5,11 +5,6 @@ import androidx.core.net.toFile
import co.touchlab.kermit.Logger
import com.github.junrar.Archive
import com.hippo.unifile.UniFile
import dev.yokai.core.metadata.COMIC_INFO_FILE
import dev.yokai.core.metadata.ComicInfo
import dev.yokai.core.metadata.copyFromComicInfo
import dev.yokai.core.metadata.toComicInfo
import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
@ -37,10 +32,15 @@ import nl.adaptivity.xmlutil.serialization.XML
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.core.metadata.COMIC_INFO_FILE
import yokai.core.metadata.ComicInfo
import yokai.core.metadata.copyFromComicInfo
import yokai.core.metadata.toComicInfo
import yokai.domain.storage.StorageManager
import java.io.FileInputStream
import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import java.util.concurrent.*
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
companion object {

View file

@ -1,106 +0,0 @@
package eu.kanade.tachiyomi.source
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.awaitSingle
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/**
* A basic interface for creating a source. It could be an online source, a local source, etc.
*/
interface Source {
/**
* ID for the source. Must be unique.
*/
val id: Long
/**
* Name of the source.
*/
val name: String
val lang: String
get() = ""
/**
* Get the updated details for a manga.
*
* @since extensions-lib 1.5
* @param manga the manga to update.
* @return the updated manga.
*/
@Suppress("DEPRECATION")
suspend fun getMangaDetails(manga: SManga): SManga {
return fetchMangaDetails(manga).awaitSingle()
}
/**
* Get all the available chapters for a manga.
*
* @since extensions-lib 1.5
* @param manga the manga to update.
* @return the chapters for the manga.
*/
@Suppress("DEPRECATION")
suspend fun getChapterList(manga: SManga): List<SChapter> {
return fetchChapterList(manga).awaitSingle()
}
/**
* Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored.
*
* @since extensions-lib 1.5
* @param chapter the chapter.
* @return the pages for the chapter.
*/
@Suppress("DEPRECATION")
suspend fun getPageList(chapter: SChapter): List<Page> {
return fetchPageList(chapter).awaitSingle()
}
fun includeLangInName(enabledLanguages: Set<String>, extensionManager: ExtensionManager? = null): Boolean {
val httpSource = this as? HttpSource ?: return true
val extManager = extensionManager ?: Injekt.get()
val allExt = httpSource.getExtension(extManager)?.lang == "all"
val onlyAll = httpSource.extOnlyHasAllLanguage(extManager)
val isMultiLingual = enabledLanguages.filterNot { it == "all" }.size > 1
return (isMultiLingual && allExt) || (lang == "all" && !onlyAll)
}
fun nameBasedOnEnabledLanguages(enabledLanguages: Set<String>, extensionManager: ExtensionManager? = null): String {
return if (includeLangInName(enabledLanguages, extensionManager)) toString() else name
}
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getMangaDetails"),
)
fun fetchMangaDetails(manga: SManga): Observable<SManga> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getChapterList"),
)
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
throw IllegalStateException("Not used")
@Deprecated(
"Use the non-RxJava API instead",
ReplaceWith("getPageList"),
)
fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
throw IllegalStateException("Not used")
}
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
fun Source.preferenceKey(): String = "source_$id"

View file

@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.source
import android.graphics.drawable.Drawable
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
fun Source.includeLangInName(enabledLanguages: Set<String>, extensionManager: ExtensionManager? = null): Boolean {
val httpSource = this as? HttpSource ?: return true
val extManager = extensionManager ?: Injekt.get()
val allExt = httpSource.getExtension(extManager)?.lang == "all"
val onlyAll = httpSource.extOnlyHasAllLanguage(extManager)
val isMultiLingual = enabledLanguages.filterNot { it == "all" }.size > 1
return (isMultiLingual && allExt) || (lang == "all" && !onlyAll)
}
fun Source.nameBasedOnEnabledLanguages(enabledLanguages: Set<String>, extensionManager: ExtensionManager? = null): String {
return if (includeLangInName(enabledLanguages, extensionManager)) toString() else name
}
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
fun HttpSource.getExtension(extensionManager: ExtensionManager? = null): Extension.Installed? =
(extensionManager ?: Injekt.get()).installedExtensionsFlow.value.find { it.sources.contains(this) }
fun HttpSource.extOnlyHasAllLanguage(extensionManager: ExtensionManager? = null) =
getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true

View file

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.source
/**
* A factory for creating sources at runtime.
*/
interface SourceFactory {
/**
* Create a new copy of the sources
* @return The created sources
*/
fun createSources(): List<Source>
}

View file

@ -1,8 +0,0 @@
package eu.kanade.tachiyomi.source
/**
* A source that explicitly doesn't require traffic considerations.
*
* This typically applies for self-hosted sources.
*/
interface UnmeteredSource

View file

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.source.model
sealed class Filter<T>(val name: String, var state: T) {
open class Header(name: String) : Filter<Any>(name, 0)
open class Separator(name: String = "") : Filter<Any>(name, 0)
abstract class Select<V>(name: String, val values: Array<V>, state: Int = 0) : Filter<Int>(name, state)
abstract class Text(name: String, state: String = "") : Filter<String>(name, state)
abstract class CheckBox(name: String, state: Boolean = false) : Filter<Boolean>(name, state)
abstract class TriState(name: String, state: Int = STATE_IGNORE) : Filter<Int>(name, state) {
fun isIgnored() = state == STATE_IGNORE
fun isIncluded() = state == STATE_INCLUDE
fun isExcluded() = state == STATE_EXCLUDE
companion object {
const val STATE_IGNORE = 0
const val STATE_INCLUDE = 1
const val STATE_EXCLUDE = 2
}
}
abstract class Group<V>(name: String, state: List<V>) : Filter<List<V>>(name, state)
abstract class Sort(name: String, val values: Array<String>, state: Selection? = null) :
Filter<Sort.Selection?>(name, state) {
data class Selection(val index: Int, val ascending: Boolean)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Filter<*>) return false
return name == other.name && state == other.state
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + (state?.hashCode() ?: 0)
return result
}
}

View file

@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.source.model
data class FilterList(val list: List<Filter<*>>) : List<Filter<*>> by list {
constructor(vararg fs: Filter<*>) : this(if (fs.isNotEmpty()) fs.asList() else emptyList())
}

View file

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.source.model
data class MangasPage(val mangas: List<SManga>, val hasNextPage: Boolean)

View file

@ -1,58 +0,0 @@
package eu.kanade.tachiyomi.source.model
import android.net.Uri
import eu.kanade.tachiyomi.network.ProgressListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
open class Page(
val index: Int,
val url: String = "",
var imageUrl: String? = null,
@Transient var uri: Uri? = null, // Deprecated but can't be deleted due to extensions
) : ProgressListener {
val number: Int
get() = index + 1
@Transient
private val _statusFlow = MutableStateFlow(State.QUEUE)
@Transient
val statusFlow = _statusFlow.asStateFlow()
var status: State
get() = _statusFlow.value
set(value) {
_statusFlow.value = value
}
@Transient
private val _progressFlow = MutableStateFlow(0)
@Transient
val progressFlow = _progressFlow.asStateFlow()
var progress: Int
get() = _progressFlow.value
set(value) {
_progressFlow.value = value
}
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
}
enum class State {
QUEUE,
LOAD_PAGE,
DOWNLOAD_IMAGE,
READY,
ERROR,
}
}

View file

@ -1,41 +0,0 @@
package eu.kanade.tachiyomi.source.model
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import java.io.Serializable
interface SChapter : Serializable {
var url: String
var name: String
var date_upload: Long
var chapter_number: Float
var scanlator: String?
fun copyFrom(other: SChapter) {
name = other.name
url = other.url
date_upload = other.date_upload
chapter_number = other.chapter_number
scanlator = other.scanlator
}
fun toChapter(): ChapterImpl {
return ChapterImpl().apply {
name = this@SChapter.name
url = this@SChapter.url
date_upload = this@SChapter.date_upload
chapter_number = this@SChapter.chapter_number
scanlator = this@SChapter.scanlator
}
}
companion object {
fun create(): SChapter {
return SChapterImpl()
}
}
}

View file

@ -1,14 +0,0 @@
package eu.kanade.tachiyomi.source.model
class SChapterImpl : SChapter {
override lateinit var url: String
override lateinit var name: String
override var date_upload: Long = 0
override var chapter_number: Float = -1f
override var scanlator: String? = null
}

View file

@ -1,100 +0,0 @@
package eu.kanade.tachiyomi.source.model
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import java.io.Serializable
interface SManga : Serializable {
var url: String
var title: String
var artist: String?
var author: String?
var description: String?
var genre: String?
var status: Int
var thumbnail_url: String?
var update_strategy: UpdateStrategy
var initialized: Boolean
val originalTitle: String
get() = (this as? MangaImpl)?.ogTitle ?: title
val originalAuthor: String?
get() = (this as? MangaImpl)?.ogAuthor ?: author
val originalArtist: String?
get() = (this as? MangaImpl)?.ogArtist ?: artist
val originalDescription: String?
get() = (this as? MangaImpl)?.ogDesc ?: description
val originalGenre: String?
get() = (this as? MangaImpl)?.ogGenre ?: genre
val originalStatus: Int
get() = (this as? MangaImpl)?.ogStatus ?: status
val hasSameAuthorAndArtist: Boolean
get() = author == artist || artist.isNullOrBlank() ||
author?.contains(artist ?: "", true) == true
fun copyFrom(other: SManga) {
if (other.author != null) {
author = other.originalAuthor
}
if (other.artist != null) {
artist = other.originalArtist
}
if (other.description != null) {
description = other.originalDescription
}
if (other.genre != null) {
genre = other.originalGenre
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.originalStatus
update_strategy = other.update_strategy
if (!initialized) {
initialized = other.initialized
}
}
fun copy() = create().also {
it.url = url
it.title = title
it.artist = artist
it.author = author
it.description = description
it.genre = genre
it.status = status
it.thumbnail_url = thumbnail_url
it.initialized = initialized
}
companion object {
const val UNKNOWN = 0
const val ONGOING = 1
const val COMPLETED = 2
const val LICENSED = 3
const val PUBLISHING_FINISHED = 4
const val CANCELLED = 5
const val ON_HIATUS = 6
fun create(): SManga {
return MangaImpl()
}
}
}

View file

@ -1,22 +0,0 @@
package eu.kanade.tachiyomi.source.model
/**
* Define the update strategy for a single [SManga].
* The strategy used will only take effect on the library update.
*
* @since extensions-lib 1.4
*/
enum class UpdateStrategy {
/**
* Series marked as always update will be included in the library
* update if they aren't excluded by additional restrictions.
*/
ALWAYS_UPDATE,
/**
* Series marked as only fetch once will be automatically skipped
* during library updates. Useful for cases where the series is previously
* known to be finished and have only a single chapter, for example.
*/
ONLY_FETCH_ONCE,
}

View file

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.source.models
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.model.SManga
val SManga.originalTitle: String
get() = if (this is Manga) this.originalTitle else title

View file

@ -1,514 +0,0 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.getUrlWithoutDomain
import eu.kanade.tachiyomi.util.system.awaitSingle
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
/**
* A simple implementation for sources from a website.
*/
@Suppress("unused")
abstract class HttpSource : CatalogueSource {
/**
* Network service.
*/
protected val network: NetworkHelper by injectLazy()
/**
* Base url of the website without the trailing slash, like: http://mysite.com
*/
abstract val baseUrl: String
/**
* Version id used to generate the source id. If the site completely changes and urls are
* incompatible, you may increase this value and it'll be considered as a new source.
*/
open val versionId = 1
/**
* ID of the source. By default it uses a generated id using the first 16 characters (64 bits)
* of the MD5 of the string `"${name.lowercase()}/$lang/$versionId"`.
*
* The ID is generated by the [generateId] function, which can be reused if needed
* to generate outdated IDs for cases where the source name or language needs to
* be changed but migrations can be avoided.
*
* Note: the generated ID sets the sign bit to `0`.
*/
override val id by lazy { generateId(name, lang, versionId) }
/**
* Headers used for requests.
*/
val headers: Headers by lazy { headersBuilder().build() }
/**
* Default network client for doing requests.
*/
open val client: OkHttpClient
get() = network.client
/**
* Generates a unique ID for the source based on the provided [name], [lang] and
* [versionId]. It will use the first 16 characters (64 bits) of the MD5 of the string
* `"${name.lowercase()}/$lang/$versionId"`.
*
* Note: the generated ID sets the sign bit to `0`.
*
* Can be used to generate outdated IDs, such as when the source name or language
* needs to be changed but migrations can be avoided.
*
* @since extensions-lib 1.5
* @param name [String] the name of the source
* @param lang [String] the language of the source
* @param versionId [Int] the version ID of the source
* @return a unique ID for the source
*/
@Suppress("MemberVisibilityCanBePrivate")
protected fun generateId(name: String, lang: String, versionId: Int): Long {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
/**
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", network.defaultUserAgent)
}
/**
* Visible name of the source.
*/
override fun toString() = "$name (${lang.uppercase()})"
/**
* Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method.
*
* @param page the page number to retrieve.
*/
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
popularMangaParse(response)
}
}
fun getExtension(extensionManager: ExtensionManager? = null): Extension.Installed? =
(extensionManager ?: Injekt.get()).installedExtensionsFlow.value.find { it.sources.contains(this) }
fun extOnlyHasAllLanguage(extensionManager: ExtensionManager? = null) =
getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true
/**
* Returns the request for the popular manga given the page.
*
* @param page the page number to retrieve.
*/
protected abstract fun popularMangaRequest(page: Int): Request
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
protected abstract fun popularMangaParse(response: Response): MangasPage
/**
* Returns an observable containing a page with a list of manga. Normally it's not needed to
* override this method.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
return Observable.defer {
try {
client.newCall(searchMangaRequest(page, query, filters)).asObservableSuccess()
} catch (e: NoClassDefFoundError) {
// RxJava doesn't handle Errors, which tends to happen during global searches
// if an old extension using non-existent classes is still around
throw RuntimeException(e)
}
}
.map { response ->
searchMangaParse(response)
}
}
/**
* Returns the request for the search manga given the page.
*
* @param page the page number to retrieve.
* @param query the search query.
* @param filters the list of filters to apply.
*/
protected abstract fun searchMangaRequest(
page: Int,
query: String,
filters: FilterList,
): Request
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
protected abstract fun searchMangaParse(response: Response): MangasPage
/**
* Returns an observable containing a page with a list of latest manga updates.
*
* @param page the page number to retrieve.
*/
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response ->
latestUpdatesParse(response)
}
}
/**
* Returns the request for latest manga given the page.
*
* @param page the page number to retrieve.
*/
protected abstract fun latestUpdatesRequest(page: Int): Request
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
protected abstract fun latestUpdatesParse(response: Response): MangasPage
/**
* Get the updated details for a manga.
* Normally it's not needed to override this method.
*
* @param manga the manga to update.
* @return the updated manga.
*/
@Suppress("DEPRECATION")
override suspend fun getMangaDetails(manga: SManga): SManga {
return fetchMangaDetails(manga).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
}
}
/**
* Returns the request for the details of a manga. Override only if it's needed to change the
* url, send different headers or request method like POST.
*
* @param manga the manga to be updated.
*/
open fun mangaDetailsRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
protected abstract fun mangaDetailsParse(response: Response): SManga
/**
* Get all the available chapters for a manga.
* Normally it's not needed to override this method.
*
* @param manga the manga to update.
* @return the chapters for the manga.
* @throws LicensedMangaChaptersException if a manga is licensed and therefore no chapters are available.
*/
@Suppress("DEPRECATION")
override suspend fun getChapterList(manga: SManga): List<SChapter> {
if (manga.status == SManga.LICENSED) {
throw LicensedMangaChaptersException()
}
return fetchChapterList(manga).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return if (manga.status != SManga.LICENSED) {
client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response ->
chapterListParse(response)
}
} else {
Observable.error(LicensedMangaChaptersException())
}
}
/**
* Returns the request for updating the chapter list. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param manga the manga to look for chapters.
*/
protected open fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url, headers)
}
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
protected abstract fun chapterListParse(response: Response): List<SChapter>
/**
* Get the list of pages a chapter has. Pages should be returned
* in the expected order; the index is ignored.
*
* @param chapter the chapter.
* @return the pages for the chapter.
*/
@Suppress("DEPRECATION")
override suspend fun getPageList(chapter: SChapter): List<Page> {
return fetchPageList(chapter).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
pageListParse(response)
}
}
/**
* Returns the request for getting the page list. Override only if it's needed to override the
* url, send different headers or request method like POST.
*
* @param chapter the chapter whose page list has to be fetched.
*/
protected open fun pageListRequest(chapter: SChapter): Request {
return GET(baseUrl + chapter.url, headers)
}
/**
* Parses the response from the site and returns a list of pages.
*
* @param response the response from the site.
*/
protected abstract fun pageListParse(response: Response): List<Page>
/**
* Returns an observable with the page containing the source url of the image. If there's any
* error, it will return null instead of throwing an exception.
*
* @since extensions-lib 1.5
* @param page the page whose source image has to be fetched.
*/
@Suppress("DEPRECATION")
open suspend fun getImageUrl(page: Page): String {
return fetchImageUrl(page).awaitSingle()
}
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
open fun fetchImageUrl(page: Page): Observable<String> {
return client.newCall(imageUrlRequest(page))
.asObservableSuccess()
.map { imageUrlParse(it) }
}
/**
* Returns the request for getting the url to the source image. Override only if it's needed to
* override the url, send different headers or request method like POST.
*
* @param page the chapter whose page list has to be fetched
*/
protected open fun imageUrlRequest(page: Page): Request {
return GET(page.url, headers)
}
/**
* Parses the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
protected abstract fun imageUrlParse(response: Response): String
/**
* Returns the response of the source image.
* Typically does not need to be overridden.
*
* @since extensions-lib 1.5
* @param page the page whose source image has to be downloaded.
*/
open suspend fun getImage(page: Page): Response {
return client.newCachelessCallWithProgress(imageRequest(page), page)
.awaitSuccess()
}
/**
* Returns the request for getting the source image. Override only if it's needed to override
* the url, send different headers or request method like POST.
*
* @param page the chapter whose page list has to be fetched
*/
protected open fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers)
}
/**
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the chapter.
*/
fun SChapter.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
/**
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the manga.
*/
fun SManga.setUrlWithoutDomain(url: String) {
this.url = getUrlWithoutDomain(url)
}
/**
* Returns the url of the given string without the scheme and domain.
*
* @param orig the full url.
*/
private fun getUrlWithoutDomain(orig: String): String {
return try {
val uri = URI(orig.replace(" ", "%20"))
var out = uri.path
if (uri.query != null) {
out += "?" + uri.query
}
if (uri.fragment != null) {
out += "#" + uri.fragment
}
out
} catch (e: URISyntaxException) {
orig
}
}
/**
* Returns the url of the provided manga
*
* @since extensions-lib 1.4
* @param manga the manga
* @return url of the manga
*/
open fun getMangaUrl(manga: SManga): String {
return mangaDetailsRequest(manga).url.toString()
}
/**
* Returns the url of the provided chapter, default is empty to use workaround when possible
*
* @since extensions-lib 1.4
* @param chapter the chapter
* @return url of the chapter
*/
open fun getChapterUrl(chapter: SChapter): String {
return ""
}
fun getChapterUrl(manga: SManga?, chapter: SChapter): String? {
manga ?: return null
val chapterUrl = chapter.url.getUrlWithoutDomain()
val mangaUrl = getMangaUrl(manga)
return if (chapterUrl.isBlank()) {
mangaUrl
} else {
fullChapterUrl(mangaUrl, chapterUrl, chapter)
}
}
/** Helper method to handle guya-like sources */
private fun fullChapterUrl(mangaUrl: String, chapterUrl: String, chapter: SChapter): String {
val lowerUrl = baseUrl.lowercase()
return when {
chapter.url.startsWith("http") -> {
chapter.url
}
lowerUrl.contains("guya") || lowerUrl.contains("danke") ||
lowerUrl.contains("hachirumi") || lowerUrl.contains("mahoushoujobu") ||
(lowerUrl.contains("cubari") && !mangaUrl.contains("imgur")) -> {
// cubari links would have double / without the trim end
mangaUrl.trimEnd('/') + "/" + chapter.chapter_number.fmt().replace(".", "-")
}
else -> baseUrl + chapterUrl
}
}
private fun Float.fmt(): String {
return if (this == toLong().toFloat()) {
String.format("%d", toLong())
} else {
String.format("%s", this)
}
}
/**
* Called before inserting a new chapter into database. Use it if you need to override chapter
* fields, like the title or the chapter number. Do not change anything to [manga].
*
* @param chapter the chapter to be added.
* @param manga the manga of the chapter.
*/
open fun prepareNewChapter(chapter: SChapter, manga: SManga) {}
/**
* Returns the list of filters for the source.
*/
override fun getFilterList() = FilterList()
}
class LicensedMangaChaptersException : Exception("Licensed - No chapters to show")

View file

@ -1,200 +0,0 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
/**
* A simple implementation for sources from a website using Jsoup, an HTML parser.
*/
abstract class ParsedHttpSource : HttpSource() {
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(popularMangaSelector()).map { element ->
popularMangaFromElement(element)
}
val hasNextPage = popularMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
protected abstract fun popularMangaSelector(): String
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [popularMangaSelector].
*/
protected abstract fun popularMangaFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun popularMangaNextPageSelector(): String?
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(searchMangaSelector()).map { element ->
searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
protected abstract fun searchMangaSelector(): String
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [searchMangaSelector].
*/
protected abstract fun searchMangaFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun searchMangaNextPageSelector(): String?
/**
* Parses the response from the site and returns a [MangasPage] object.
*
* @param response the response from the site.
*/
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesFromElement(element)
}
val hasNextPage = latestUpdatesNextPageSelector()?.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each manga.
*/
protected abstract fun latestUpdatesSelector(): String
/**
* Returns a manga from the given [element]. Most sites only show the title and the url, it's
* totally fine to fill only those two values.
*
* @param element an element obtained from [latestUpdatesSelector].
*/
protected abstract fun latestUpdatesFromElement(element: Element): SManga
/**
* Returns the Jsoup selector that returns the <a> tag linking to the next page, or null if
* there's no next page.
*/
protected abstract fun latestUpdatesNextPageSelector(): String?
/**
* Parses the response from the site and returns the details of a manga.
*
* @param response the response from the site.
*/
override fun mangaDetailsParse(response: Response): SManga {
return mangaDetailsParse(response.asJsoup())
}
/**
* Returns the details of the manga from the given [document].
*
* @param document the parsed document.
*/
protected abstract fun mangaDetailsParse(document: Document): SManga
/**
* Parses the response from the site and returns a list of chapters.
*
* @param response the response from the site.
*/
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(chapterListSelector()).map { chapterFromElement(it) }
}
/**
* Returns the Jsoup selector that returns a list of [Element] corresponding to each chapter.
*/
protected abstract fun chapterListSelector(): String
/**
* Returns a chapter from the given element.
*
* @param element an element obtained from [chapterListSelector].
*/
protected abstract fun chapterFromElement(element: Element): SChapter
/**
* Parses the response from the site and returns the page list.
*
* @param response the response from the site.
*/
override fun pageListParse(response: Response): List<Page> {
return pageListParse(response.asJsoup())
}
/**
* Returns a page list from the given document.
*
* @param document the parsed document.
*/
protected abstract fun pageListParse(document: Document): List<Page>
/**
* Parse the response from the site and returns the absolute url to the source image.
*
* @param response the response from the site.
*/
override fun imageUrlParse(response: Response): String {
return imageUrlParse(response.asJsoup())
}
/**
* Returns the absolute url to the source image from the document.
*
* @param document the parsed document.
*/
protected abstract fun imageUrlParse(document: Document): String
}

View file

@ -4,6 +4,7 @@ import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.DelegatedHttpSource
@ -13,7 +14,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
import java.util.*
class Cubari : DelegatedHttpSource() {
override val domainName: String = "cubari"

View file

@ -4,6 +4,7 @@ import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
@ -19,7 +20,7 @@ import okhttp3.CacheControl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Locale
import java.util.*
class MangaDex : DelegatedHttpSource() {

View file

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST

View file

@ -4,6 +4,7 @@ import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toChapter
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await

View file

@ -12,7 +12,6 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import androidx.viewbinding.ViewBinding
import dev.yokai.domain.SplashState
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.SearchActivity
@ -22,6 +21,7 @@ import eu.kanade.tachiyomi.util.system.getThemeWithExtras
import eu.kanade.tachiyomi.util.system.setLocaleByAppCompat
import eu.kanade.tachiyomi.util.system.setThemeByPref
import uy.kohesive.injekt.injectLazy
import yokai.domain.SplashState
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {

View file

@ -7,7 +7,7 @@ import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import dev.yokai.presentation.theme.YokaiTheme
import yokai.presentation.theme.YokaiTheme
abstract class BaseComposeController(bundle: Bundle? = null) :
BaseController(bundle) {

View file

@ -17,9 +17,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import dev.yokai.presentation.onboarding.InfoScreen
import dev.yokai.presentation.theme.Size
import dev.yokai.presentation.theme.YokaiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.MainActivity
@ -27,6 +24,9 @@ import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.system.setThemeByPref
import kotlinx.coroutines.launch
import uy.kohesive.injekt.injectLazy
import yokai.presentation.onboarding.InfoScreen
import yokai.presentation.theme.Size
import yokai.presentation.theme.YokaiTheme
class CrashActivity : AppCompatActivity() {
internal val preferences: PreferencesHelper by injectLazy()

View file

@ -1,13 +1,13 @@
package eu.kanade.tachiyomi.ui.extension
import android.widget.TextView
import dev.yokai.domain.base.BasePreferences
import dev.yokai.domain.base.BasePreferences.ExtensionInstaller
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.extension.ExtensionAdapter.OnButtonClickListener
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import yokai.domain.base.BasePreferences.ExtensionInstaller
/**
* Adapter that holds the catalogue cards.

View file

@ -12,8 +12,6 @@ import androidx.core.view.updatePaddingRelative
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import dev.yokai.domain.base.BasePreferences
import dev.yokai.domain.base.BasePreferences.ExtensionInstaller
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -46,6 +44,8 @@ import eu.kanade.tachiyomi.util.view.withFadeTransaction
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import yokai.domain.base.BasePreferences.ExtensionInstaller
class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs),

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.library
import android.os.Build
import android.view.HapticFeedbackConstants
import android.view.View
import dev.yokai.domain.ui.UiPreferences
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -18,7 +17,8 @@ import eu.kanade.tachiyomi.util.system.timeSpanFromNow
import eu.kanade.tachiyomi.util.system.withDefContext
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.injectLazy
import java.util.Locale
import yokai.domain.ui.UiPreferences
import java.util.*
/**
* Adapter storing a list of manga in a certain category.

View file

@ -51,7 +51,6 @@ import com.bluelinelabs.conductor.ControllerChangeType
import com.github.florent37.viewtooltip.ViewTooltip
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import dev.yokai.domain.ui.UiPreferences
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.items.IFlexible
@ -130,7 +129,8 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Locale
import yokai.domain.ui.UiPreferences
import java.util.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt

View file

@ -11,7 +11,6 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import dev.yokai.domain.ui.UiPreferences
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFilterable
@ -27,6 +26,7 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences
class LibraryItem(
val manga: LibraryManga,

View file

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.ui.library
import dev.yokai.domain.chapter.interactor.GetChapters
import dev.yokai.domain.manga.interactor.GetLibraryManga
import dev.yokai.util.isLewd
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.preference.minusAssign
import eu.kanade.tachiyomi.core.preference.plusAssign
@ -57,8 +54,11 @@ import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetChapters
import yokai.domain.manga.interactor.GetLibraryManga
import yokai.util.isLewd
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.*
import kotlin.math.roundToInt
import kotlin.random.Random

View file

@ -67,10 +67,6 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import com.google.common.primitives.Floats.max
import com.google.common.primitives.Ints.max
import dev.yokai.core.migration.Migrator
import dev.yokai.domain.base.BasePreferences
import dev.yokai.presentation.extension.repo.ExtensionRepoController
import dev.yokai.presentation.onboarding.OnboardingController
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -143,20 +139,10 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
import kotlin.collections.List
import kotlin.collections.MutableList
import kotlin.collections.MutableMap
import kotlin.collections.distinct
import kotlin.collections.filterNotNull
import kotlin.collections.firstOrNull
import kotlin.collections.forEach
import kotlin.collections.forEachIndexed
import kotlin.collections.lastOrNull
import kotlin.collections.listOf
import kotlin.collections.map
import kotlin.collections.maxByOrNull
import kotlin.collections.orEmpty
import kotlin.collections.plus
import yokai.core.migration.Migrator
import yokai.domain.base.BasePreferences
import yokai.presentation.extension.repo.ExtensionRepoController
import yokai.presentation.onboarding.OnboardingController
import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.min

View file

@ -8,7 +8,6 @@ import androidx.core.view.isVisible
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.SimpleSwapChangeHandler
import dev.yokai.domain.chapter.interactor.GetChapters
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -30,6 +29,7 @@ import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetChapters
class SearchActivity : MainActivity() {
private val getChapters: GetChapters by injectLazy()

View file

@ -10,10 +10,6 @@ import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import com.hippo.unifile.UniFile
import dev.yokai.domain.chapter.interactor.GetAvailableScanlators
import dev.yokai.domain.chapter.interactor.GetChapters
import dev.yokai.domain.library.custom.model.CustomMangaInfo
import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
@ -36,6 +32,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.SourceNotFoundException
import eu.kanade.tachiyomi.source.getExtension
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
@ -74,11 +71,14 @@ import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetAvailableScanlators
import yokai.domain.chapter.interactor.GetChapters
import yokai.domain.library.custom.model.CustomMangaInfo
import yokai.domain.storage.StorageManager
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.util.Date
import java.util.Locale
import java.util.*
class MangaDetailsPresenter(
val manga: Manga,

View file

@ -26,17 +26,18 @@ import androidx.core.widget.TextViewCompat
import androidx.transition.TransitionSet
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import coil3.request.CachePolicy
import coil3.request.placeholder
import coil3.request.error
import coil3.request.placeholder
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.coil.loadManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.ChapterHeaderItemBinding
import eu.kanade.tachiyomi.databinding.MangaHeaderItemBinding
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.isLocal
import eu.kanade.tachiyomi.util.lang.toNormalized

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.migration
import dev.yokai.domain.ui.UiPreferences
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues
@ -18,6 +17,7 @@ import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.ui.UiPreferences
abstract class BaseMigrationPresenter<T : BaseMigrationInterface>(
protected val sourceManager: SourceManager = Injekt.get(),

View file

@ -4,6 +4,7 @@ import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.view.View
import eu.kanade.tachiyomi.databinding.MigrationSourceItemBinding
import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder

View file

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.migration.manga.process
import android.view.MenuItem
import dev.yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
import dev.yokai.domain.ui.UiPreferences
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -24,7 +22,9 @@ import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Date
import yokai.domain.library.custom.model.CustomMangaInfo.Companion.getMangaInfo
import yokai.domain.ui.UiPreferences
import java.util.*
class MigrationProcessAdapter(
val controller: MigrationListController,

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.more.stats
import dev.yokai.domain.manga.interactor.GetLibraryManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.LibraryManga
@ -21,6 +20,7 @@ import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.manga.interactor.GetLibraryManga
/**
* Presenter of [StatsController].

View file

@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.databinding.StatsDetailsChartBinding
import eu.kanade.tachiyomi.databinding.StatsDetailsControllerBinding
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
import eu.kanade.tachiyomi.ui.library.FilteredLibraryController
@ -59,8 +60,7 @@ import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.Locale
import java.util.*
class StatsDetailsController :
BaseCoroutineController<StatsDetailsControllerBinding, StatsDetailsPresenter>(),

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.more.stats.details
import android.graphics.drawable.Drawable
import android.text.format.DateUtils
import androidx.annotation.DrawableRes
import dev.yokai.domain.manga.interactor.GetLibraryManga
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
import eu.kanade.tachiyomi.ui.more.stats.StatsHelper
import eu.kanade.tachiyomi.util.isLocal
@ -32,9 +32,9 @@ import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
import yokai.domain.manga.interactor.GetLibraryManga
import java.util.*
import java.util.concurrent.*
import kotlin.math.roundToInt
class StatsDetailsPresenter(

View file

@ -75,9 +75,6 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.platform.MaterialContainerTransform
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import com.hippo.unifile.UniFile
import dev.yokai.domain.base.BasePreferences
import dev.yokai.domain.ui.settings.ReaderPreferences
import dev.yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.preference.toggle
@ -161,6 +158,9 @@ import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import yokai.domain.ui.settings.ReaderPreferences
import yokai.domain.ui.settings.ReaderPreferences.LandscapeCutoutBehaviour
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols

View file

@ -9,9 +9,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import dev.yokai.domain.chapter.interactor.GetChapters
import dev.yokai.domain.download.DownloadPreferences
import dev.yokai.domain.storage.StorageManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -76,8 +73,11 @@ import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetChapters
import yokai.domain.download.DownloadPreferences
import yokai.domain.storage.StorageManager
import java.util.*
import java.util.concurrent.CancellationException
import java.util.concurrent.*
/**
* Presenter used by the activity to perform background operations.

View file

@ -29,7 +29,6 @@ import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE
import com.github.chrisbanes.photoview.PhotoView
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
import eu.kanade.tachiyomi.data.coil.cropBorders
import eu.kanade.tachiyomi.data.coil.customDecoder
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
@ -39,6 +38,7 @@ import eu.kanade.tachiyomi.util.system.GLUtil
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import okio.BufferedSource
import yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
/**
* A wrapper view for showing page image.

View file

@ -17,8 +17,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.isVisible
import dev.yokai.presentation.component.CombinedCircularProgressIndicator
import dev.yokai.presentation.theme.YokaiTheme
import yokai.presentation.component.CombinedCircularProgressIndicator
import yokai.presentation.theme.YokaiTheme
/**
* A custom progress bar that always rotates while being determinate. By always rotating we give

View file

@ -1,8 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import dev.yokai.domain.ui.settings.ReaderPreferences
import dev.yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.reader.settings.PageLayout
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerConfig
@ -12,13 +10,14 @@ import eu.kanade.tachiyomi.ui.reader.viewer.navigation.EdgeNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.KindlishNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.LNavigation
import eu.kanade.tachiyomi.ui.reader.viewer.navigation.RightAndLeftNavigation
import eu.kanade.tachiyomi.util.system.DeviceUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import yokai.domain.ui.settings.ReaderPreferences
import yokai.domain.ui.settings.ReaderPreferences.CutoutBehaviour
/**
* Configuration used by pager viewers.

View file

@ -12,7 +12,6 @@ import androidx.appcompat.widget.AppCompatTextView
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.updatePaddingRelative
import dev.yokai.presentation.theme.YokaiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -24,6 +23,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import yokai.presentation.theme.YokaiTheme
/**
* View of the ViewPager that contains a chapter transition.

View file

@ -11,7 +11,6 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.isNotEmpty
import androidx.core.view.isVisible
import dev.yokai.presentation.theme.YokaiTheme
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -21,6 +20,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import yokai.presentation.theme.YokaiTheme
/**
* Holder of the webtoon viewer that contains a chapter transition.

View file

@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.ui.recents
import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import dev.yokai.domain.recents.RecentsPreferences
import dev.yokai.domain.ui.UiPreferences
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.core.preference.Preference
import eu.kanade.tachiyomi.data.database.models.Chapter
@ -16,10 +14,12 @@ import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
import yokai.domain.recents.RecentsPreferences
import yokai.domain.ui.UiPreferences
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.*
class RecentMangaAdapter(val delegate: RecentsInterface) :
BaseChapterAdapter<IFlexible<*>>(delegate) {

View file

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.ui.recents
import dev.yokai.domain.chapter.interactor.GetChapters
import dev.yokai.domain.recents.RecentsPreferences
import dev.yokai.domain.ui.UiPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
@ -37,12 +34,12 @@ import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.chapter.interactor.GetChapters
import yokai.domain.recents.RecentsPreferences
import yokai.domain.ui.UiPreferences
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TreeMap
import java.util.concurrent.TimeUnit
import java.util.*
import java.util.concurrent.*
import kotlin.math.abs
import kotlin.math.roundToInt

View file

@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.setting
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import dev.yokai.domain.ComposableAlertDialog
import dev.yokai.presentation.settings.ComposableSettings
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
import eu.kanade.tachiyomi.util.compose.LocalBackPress
import yokai.domain.ComposableAlertDialog
import yokai.presentation.settings.ComposableSettings
abstract class SettingsComposeController: BaseComposeController(), SettingsControllerInterface {
override fun getTitle(): String? = __getTitle()

View file

@ -16,7 +16,6 @@ import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import dev.yokai.domain.base.BasePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.changesIn
@ -30,6 +29,7 @@ import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.widget.LinearLayoutManagerAccurateOffset
import kotlinx.coroutines.MainScope
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences
import java.util.*
abstract class SettingsLegacyController : PreferenceController(), SettingsControllerInterface, BackHandlerControllerInterface, BaseControllerPreferenceControllerCommonInterface {

View file

@ -16,8 +16,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceScreen
import co.touchlab.kermit.Logger
import com.hippo.unifile.UniFile
import dev.yokai.domain.base.BasePreferences.ExtensionInstaller
import dev.yokai.domain.extension.interactor.TrustExtension
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -29,6 +27,7 @@ import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.changesIn
import eu.kanade.tachiyomi.extension.ShizukuInstaller
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
@ -78,11 +77,14 @@ import rikka.sui.Sui
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences.ExtensionInstaller
import yokai.domain.extension.interactor.TrustExtension
import java.io.File
class SettingsAdvancedController : SettingsLegacyController() {
private val network: NetworkHelper by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy()
private val db: DatabaseHelper by injectLazy()
@ -278,7 +280,7 @@ class SettingsAdvancedController : SettingsLegacyController() {
}
}
editTextPreference(activity) {
bindTo(preferences.defaultUserAgent())
bindTo(networkPreferences.defaultUserAgent())
titleRes = R.string.user_agent_string
onChange {

View file

@ -6,8 +6,6 @@ import android.os.Build
import android.provider.Settings
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import dev.yokai.domain.base.BasePreferences.ExtensionInstaller
import dev.yokai.presentation.extension.repo.ExtensionRepoController
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
@ -35,6 +33,8 @@ import eu.kanade.tachiyomi.util.lang.addBetaTag
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import uy.kohesive.injekt.injectLazy
import yokai.domain.base.BasePreferences.ExtensionInstaller
import yokai.presentation.extension.repo.ExtensionRepoController
class SettingsBrowseController : SettingsLegacyController() {

View file

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.ui.setting.controllers
import dev.yokai.presentation.settings.ComposableSettings
import dev.yokai.presentation.settings.screen.SettingsDataScreen
import eu.kanade.tachiyomi.ui.setting.SettingsComposeController
import yokai.presentation.settings.ComposableSettings
import yokai.presentation.settings.screen.SettingsDataScreen
class SettingsDataController : SettingsComposeController() {
override fun getComposableSettings(): ComposableSettings = SettingsDataScreen

View file

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.setting.controllers
import android.content.Intent
import androidx.preference.PreferenceScreen
import com.bluelinelabs.conductor.Controller
import dev.yokai.domain.download.DownloadPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -22,6 +21,7 @@ import eu.kanade.tachiyomi.ui.setting.triStateListPreference
import eu.kanade.tachiyomi.util.lang.addBetaTag
import eu.kanade.tachiyomi.util.view.withFadeTransaction
import uy.kohesive.injekt.injectLazy
import yokai.domain.download.DownloadPreferences
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsDownloadController : SettingsLegacyController() {

View file

@ -9,7 +9,6 @@ import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceScreen
import dev.yokai.domain.base.BasePreferences
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
@ -30,7 +29,8 @@ import eu.kanade.tachiyomi.ui.setting.titleRes
import eu.kanade.tachiyomi.util.lang.addBetaTag
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.systemLangContext
import java.util.Locale
import yokai.domain.base.BasePreferences
import java.util.*
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsGeneralController : SettingsLegacyController() {

Some files were not shown because too many files have changed in this diff Show more