Extract source api from app module (#8014)

* Extract source api from app module

* Extract source online api from app module
This commit is contained in:
Andreas 2022-09-16 00:12:27 +02:00 committed by GitHub
parent 30ac94181b
commit 86fe850794
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 219 additions and 106 deletions

View file

@ -150,6 +150,8 @@ android {
dependencies { dependencies {
implementation(project(":i18n")) implementation(project(":i18n"))
implementation(project(":core"))
implementation(project(":source-api"))
// Compose // Compose
implementation(compose.activity) implementation(compose.activity)

View file

@ -33,6 +33,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.copyFrom
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toLong import eu.kanade.tachiyomi.util.system.toLong
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf

View file

@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.isDevFlavor import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import java.io.File import java.io.File
import java.text.DateFormat import java.text.DateFormat

View file

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.source
import android.graphics.drawable.Drawable
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
fun Source.getPreferenceKey(): String = "source_$id"
fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name)
fun Source.getNameForMangaInfo(): String {
val preferences = Injekt.get<PreferencesHelper>()
val enabledLanguages = preferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
val isInEnabledLanguages = lang in enabledLanguages
return when {
// For edge cases where user disables a source they got manga of in their library.
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
// Hide the language tag when only one language is used.
hasOneActiveLanguages && isInEnabledLanguages -> name
else -> toString()
}
}
fun Source.isLocalOrStub(): Boolean = id == LocalSource.ID || this is SourceManager.StubSource

View file

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.source.model
import data.Chapters
fun SChapter.copyFrom(other: Chapters) {
name = other.name
url = other.url
date_upload = other.date_upload
chapter_number = other.chapter_number
scanlator = other.scanlator
}

View file

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.source.model
import data.Mangas
fun SManga.copyFrom(other: Mangas) {
if (other.author != null) {
author = other.author
}
if (other.artist != null) {
artist = other.artist
}
if (other.description != null) {
description = other.description
}
if (other.genre != null) {
genre = other.genre.joinToString(separator = ", ")
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.status.toInt()
if (!initialized) {
initialized = other.initialized
}
}

View file

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import eu.kanade.tachiyomi.util.system.isTablet import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.widget.preference.ThemesPreference import eu.kanade.tachiyomi.widget.preference.ThemesPreference
import java.util.Date import java.util.Date

View file

@ -24,10 +24,8 @@ import android.util.TypedValue
import android.view.Display import android.view.Display
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.StringRes
import androidx.appcompat.view.ContextThemeWrapper import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -52,29 +50,6 @@ import kotlin.math.roundToInt
private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720 private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720
/**
* Display a toast in this context.
*
* @param resource the text resource.
* @param duration the duration of the toast. Defaults to short.
*/
fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
return toast(getString(resource), duration, block)
}
/**
* Display a toast in this context.
*
* @param text the text to display.
* @param duration the duration of the toast. Defaults to short.
*/
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
return Toast.makeText(applicationContext, text.orEmpty(), duration).also {
block(it)
it.show()
}
}
/** /**
* Copies a string to clipboard * Copies a string to clipboard
* *

View file

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.util.system
import android.os.Build
import com.google.android.material.color.DynamicColors
val DeviceUtil.isDynamicColorAvailable by lazy {
DynamicColors.isDynamicColorAvailable() || (DeviceUtil.isSamsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
}

1
core/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

46
core/build.gradle.kts Normal file
View file

@ -0,0 +1,46 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.serialization")
}
android {
namespace = "eu.kanade.tachiyomi.core"
compileSdk = AndroidConfig.compileSdk
defaultConfig {
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation(project(":i18n"))
api(libs.logcat)
api(libs.rxjava)
api(libs.okhttp.core)
api(libs.okhttp.logging)
api(libs.okhttp.dnsoverhttps)
api(libs.okio)
api(kotlinx.coroutines.core)
api(kotlinx.serialization.json)
api(libs.injekt.core)
api(libs.preferencektx)
implementation(androidx.corektx)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View file

@ -1,20 +1,20 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.Http103Interceptor import eu.kanade.tachiyomi.network.interceptor.Http103Interceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
private val preferences: PreferencesHelper by injectLazy() // TODO: Abstract preferences similar to 1.x
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val cacheDir = File(context.cacheDir, "network_cache") private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB private val cacheSize = 5L * 1024 * 1024 // 5 MiB
@ -36,14 +36,14 @@ class NetworkHelper(context: Context) {
.addInterceptor(userAgentInterceptor) .addInterceptor(userAgentInterceptor)
.addNetworkInterceptor(http103Interceptor) .addNetworkInterceptor(http103Interceptor)
if (preferences.verboseLogging()) { if (preferences.getBoolean("verbose_logging", false)) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply { val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS level = HttpLoggingInterceptor.Level.HEADERS
} }
builder.addNetworkInterceptor(httpLoggingInterceptor) builder.addNetworkInterceptor(httpLoggingInterceptor)
} }
when (preferences.dohProvider()) { when (preferences.getInt("doh_provider", -1)) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle() PREF_DOH_GOOGLE -> builder.dohGoogle()
PREF_DOH_ADGUARD -> builder.dohAdGuard() PREF_DOH_ADGUARD -> builder.dohAdGuard()
@ -70,6 +70,6 @@ class NetworkHelper(context: Context) {
} }
val defaultUserAgent by lazy { val defaultUserAgent by lazy {
preferences.defaultUserAgent().get() preferences.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0")!!
} }
} }

View file

@ -10,7 +10,7 @@ import java.util.concurrent.TimeUnit.MINUTES
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
private val DEFAULT_HEADERS = Headers.Builder().build() private val DEFAULT_HEADERS = Headers.Builder().build()
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build() private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
internal val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build() val CACHE_CONTROL_NO_STORE = CacheControl.Builder().noStore().build()
fun GET( fun GET(
url: String, url: String,

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated

View file

@ -5,7 +5,7 @@ import android.os.Build
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.core.R
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil

View file

@ -5,6 +5,7 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import rx.Emitter import rx.Emitter
@ -20,6 +21,7 @@ import kotlin.coroutines.resumeWithException
suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne() suspend fun <T> Observable<T>.awaitSingle(): T = single().awaitOne()
@OptIn(InternalCoroutinesApi::class)
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont -> private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
cont.unsubscribeOnCancellation( cont.unsubscribeOnCancellation(
subscribe( subscribe(

View file

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.util.system
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import com.google.android.material.color.DynamicColors
import logcat.LogPriority import logcat.LogPriority
object DeviceUtil { object DeviceUtil {
@ -31,10 +30,6 @@ object DeviceUtil {
Build.MANUFACTURER.equals("samsung", ignoreCase = true) Build.MANUFACTURER.equals("samsung", ignoreCase = true)
} }
val isDynamicColorAvailable by lazy {
DynamicColors.isDynamicColorAvailable() || (isSamsung && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
}
val invalidDefaultBrowsers = listOf("android", "com.huawei.android.internal.app") val invalidDefaultBrowsers = listOf("android", "com.huawei.android.internal.app")
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")

View file

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.util.system
import android.content.Context
import android.widget.Toast
import androidx.annotation.StringRes
/**
* Display a toast in this context.
*
* @param resource the text resource.
* @param duration the duration of the toast. Defaults to short.
*/
fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
return toast(getString(resource), duration, block)
}
/**
* Display a toast in this context.
*
* @param text the text to display.
* @param duration the duration of the toast. Defaults to short.
*/
fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT, block: (Toast) -> Unit = {}): Toast {
return Toast.makeText(applicationContext, text.orEmpty(), duration).also {
block(it)
it.show()
}
}

View file

@ -37,3 +37,5 @@ dependencyResolutionManagement {
rootProject.name = "Tachiyomi" rootProject.name = "Tachiyomi"
include(":app") include(":app")
include(":i18n") include(":i18n")
include(":source-api")
include(":core")

1
source-api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,39 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.serialization")
}
android {
namespace = "eu.kanade.tachiyomi.source"
compileSdk = AndroidConfig.compileSdk
defaultConfig {
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation(project(":core"))
api(kotlinx.serialization.json)
api(libs.rxjava)
api(libs.preferencektx)
api(libs.jsoup)
implementation(androidx.corektx)
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View file

@ -1,16 +1,10 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.graphics.drawable.Drawable
import eu.kanade.domain.source.model.SourceData
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import rx.Observable 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... * A basic interface for creating a source. It could be an online source, a local source, etc...
@ -88,26 +82,3 @@ interface Source {
return fetchPageList(chapter).awaitSingle() return fetchPageList(chapter).awaitSingle()
} }
} }
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
fun Source.getPreferenceKey(): String = "source_$id"
fun Source.toSourceData(): SourceData = SourceData(id = id, lang = lang, name = name)
fun Source.getNameForMangaInfo(): String {
val preferences = Injekt.get<PreferencesHelper>()
val enabledLanguages = preferences.enabledLanguages().get()
.filterNot { it in listOf("all", "other") }
val hasOneActiveLanguages = enabledLanguages.size == 1
val isInEnabledLanguages = lang in enabledLanguages
return when {
// For edge cases where user disables a source they got manga of in their library.
hasOneActiveLanguages && !isInEnabledLanguages -> toString()
// Hide the language tag when only one language is used.
hasOneActiveLanguages && isInEnabledLanguages -> name
else -> toString()
}
}
fun Source.isLocalOrStub(): Boolean = id == LocalSource.ID || this is SourceManager.StubSource

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import data.Chapters
import java.io.Serializable import java.io.Serializable
interface SChapter : Serializable { interface SChapter : Serializable {
@ -23,14 +22,6 @@ interface SChapter : Serializable {
scanlator = other.scanlator scanlator = other.scanlator
} }
fun copyFrom(other: Chapters) {
name = other.name
url = other.url
date_upload = other.date_upload
chapter_number = other.chapter_number
scanlator = other.scanlator
}
companion object { companion object {
fun create(): SChapter { fun create(): SChapter {
return SChapterImpl() return SChapterImpl()

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.source.model package eu.kanade.tachiyomi.source.model
import data.Mangas
import java.io.Serializable import java.io.Serializable
interface SManga : Serializable { interface SManga : Serializable {
@ -56,34 +55,6 @@ interface SManga : Serializable {
} }
} }
fun copyFrom(other: Mangas) {
if (other.author != null) {
author = other.author
}
if (other.artist != null) {
artist = other.artist
}
if (other.description != null) {
description = other.description
}
if (other.genre != null) {
genre = other.genre.joinToString(separator = ", ")
}
if (other.thumbnail_url != null) {
thumbnail_url = other.thumbnail_url
}
status = other.status.toInt()
if (!initialized) {
initialized = other.initialized
}
}
fun copy() = create().also { fun copy() = create().also {
it.url = url it.url = url
it.title = title it.title = title

View file

@ -12,6 +12,7 @@ import org.jsoup.nodes.Element
/** /**
* A simple implementation for sources from a website using Jsoup, an HTML parser. * A simple implementation for sources from a website using Jsoup, an HTML parser.
*/ */
@Suppress("unused")
abstract class ParsedHttpSource : HttpSource() { abstract class ParsedHttpSource : HttpSource() {
/** /**