diff --git a/app/build.gradle.kts b/app/build.gradle.kts index efd653ad48..873b469260 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 = 136 + versionCode = 137 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,9 @@ android { } dependencies { + implementation(projects.core) + implementation(projects.sourceApi) + // Compose implementation(compose.bundles.compose) debugImplementation(compose.ui.tooling) @@ -280,8 +270,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 +286,6 @@ dependencies { implementation(kotlinx.immutable) - "coreLibraryDesugaring"(libs.desugar) - // Tests testImplementation(libs.bundles.test) testRuntimeOnly(libs.bundles.test.runtime) @@ -306,13 +294,6 @@ dependencies { } tasks { - withType { - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - } - } - // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) withType { kotlinOptions.freeCompilerArgs += listOf( diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 7d18bb92e9..da1cf93e81 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -4,6 +4,7 @@ -keep,allowoptimization class eu.kanade.** { public protected *; } -keep,allowoptimization class tachiyomi.** { public protected *; } -keep,allowoptimization class dev.yokai.** { public protected *; } +-keep,allowoptimization class yokai.** { public protected *; } # Keep common dependencies used in extensions -keep class androidx.preference.** { public protected *; } diff --git a/app/src/main/java/dev/yokai/core/di/AppModule.kt b/app/src/main/java/dev/yokai/core/di/AppModule.kt index fd2081855f..3edafc645a 100644 --- a/app/src/main/java/dev/yokai/core/di/AppModule.kt +++ b/app/src/main/java/dev/yokai/core/di/AppModule.kt @@ -5,11 +5,13 @@ import androidx.core.content.ContextCompat import androidx.sqlite.db.SupportSQLiteOpenHelper import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import com.chuckerteam.chucker.api.ChuckerCollector +import com.chuckerteam.chucker.api.ChuckerInterceptor import dev.yokai.data.AndroidDatabaseHandler import dev.yokai.data.DatabaseHandler import dev.yokai.domain.SplashState -import dev.yokai.domain.extension.interactor.TrustExtension import dev.yokai.domain.storage.StorageManager +import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.core.storage.AndroidStorageFolderProvider import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.CoverCache @@ -89,7 +91,23 @@ class AppModule(val app: Application) : InjektModule { addSingletonFactory { CoverCache(app) } - addSingletonFactory { NetworkHelper(app) } + addSingletonFactory { + NetworkHelper( + app, + get(), + ) { builder -> + if (BuildConfig.DEBUG) { + builder.addInterceptor( + ChuckerInterceptor.Builder(app) + .collector(ChuckerCollector(app)) + .maxContentLength(250000L) + .redactHeaders(emptySet()) + .alwaysReadResponseBody(false) + .build(), + ) + } + } + } addSingletonFactory { JavaScriptEngine(app) } diff --git a/app/src/main/java/dev/yokai/core/di/PreferenceModule.kt b/app/src/main/java/dev/yokai/core/di/PreferenceModule.kt index 10698079c0..f926828a56 100644 --- a/app/src/main/java/dev/yokai/core/di/PreferenceModule.kt +++ b/app/src/main/java/dev/yokai/core/di/PreferenceModule.kt @@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.core.storage.AndroidStorageFolderProvider import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackPreferences +import eu.kanade.tachiyomi.network.NetworkPreferences import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.addSingletonFactory @@ -36,6 +37,8 @@ class PreferenceModule(val application: Application) : InjektModule { addSingletonFactory { DownloadPreferences(get()) } + addSingletonFactory { NetworkPreferences(get()) } + addSingletonFactory { PreferencesHelper( context = application, diff --git a/app/src/main/java/dev/yokai/core/migration/migrations/Migrations.kt b/app/src/main/java/dev/yokai/core/migration/migrations/Migrations.kt index db933c0c41..4acbc83755 100644 --- a/app/src/main/java/dev/yokai/core/migration/migrations/Migrations.kt +++ b/app/src/main/java/dev/yokai/core/migration/migrations/Migrations.kt @@ -36,4 +36,5 @@ val migrations: ImmutableList = persistentListOf( ExtensionInstallerEnumMigration(), CutoutMigration(), RepoJsonMigration(), + NetworkPrefsMigration(), ) diff --git a/app/src/main/java/dev/yokai/core/migration/migrations/NetworkPrefsMigration.kt b/app/src/main/java/dev/yokai/core/migration/migrations/NetworkPrefsMigration.kt new file mode 100644 index 0000000000..f52aed4ddd --- /dev/null +++ b/app/src/main/java/dev/yokai/core/migration/migrations/NetworkPrefsMigration.kt @@ -0,0 +1,23 @@ +package dev.yokai.core.migration.migrations + +import androidx.preference.PreferenceManager +import dev.yokai.core.migration.Migration +import dev.yokai.core.migration.MigrationContext +import eu.kanade.tachiyomi.App +import eu.kanade.tachiyomi.network.NetworkPreferences + +class NetworkPrefsMigration : Migration { + override val version: Float = 137f + + override suspend fun invoke(migrationContext: MigrationContext): Boolean { + val context: App = migrationContext.get() ?: return false + val preferences: NetworkPreferences = migrationContext.get() ?: return false + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + + val dohProvider = prefs.getInt("doh_provider", -1) + if (dohProvider > -1) { + preferences.dohProvider().set(dohProvider) + } + return true + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt b/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt new file mode 100644 index 0000000000..be0b8e9773 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/core/storage/preference/PreferenceExtension.kt @@ -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 Preference.collectAsState(): State { + val flow = remember(this) { changes() } + return flow.collectAsState(initial = get()) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt index 0daae16f5d..7017d0d14c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/ChapterImpl.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 4c63956f70..26c4c5f5fd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -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,53 @@ 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: 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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 02e6693495..e86b2da262 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt new file mode 100644 index 0000000000..4cbf8218fc --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceExtensions.kt @@ -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, 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, extensionManager: ExtensionManager? = null): String { + return if (includeLangInName(enabledLanguages, extensionManager)) toString() else name +} + +fun Source.icon(): Drawable? = Injekt.get().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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt deleted file mode 100644 index 681cf126f4..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ /dev/null @@ -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() - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt index 79b24f74a0..fa460c9e87 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/Cubari.kt @@ -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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 1b98e01ad5..0f80b5b601 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -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() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt index 3f7b595e68..0f006897af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/FoolSlide.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt index 631e0efdf1..7a03655487 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/english/MangaPlus.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt index 55b8d93f23..ea27eca7bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/controllers/SettingsAdvancedController.kt @@ -29,6 +29,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 @@ -83,6 +84,7 @@ 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 { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 621f3f3ab0..25187628be 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -19,12 +19,10 @@ import android.os.PowerManager import android.provider.Settings import android.util.TypedValue import android.view.View -import android.widget.Toast import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatDelegate import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent @@ -54,26 +52,6 @@ import kotlin.math.max 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) { - Toast.makeText(this, resource, duration).show() -} - -/** - * 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) { - Toast.makeText(this, text.orEmpty(), duration).show() -} - /** * Helper method to create a notification. * diff --git a/build.gradle.kts b/build.gradle.kts index d97f392324..38914a27f1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,7 @@ +import com.android.build.gradle.BaseExtension +import com.android.build.gradle.BasePlugin +import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile import java.util.* plugins { @@ -23,6 +27,45 @@ buildscript { } } +subprojects { + tasks.withType { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + } + + tasks.withType { + useJUnitPlatform() + testLogging { + events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } + } + + plugins.withType { + configure { + compileSdkVersion(AndroidConfig.compileSdk) + + defaultConfig { + minSdk = AndroidConfig.minSdk + targetSdk = AndroidConfig.targetSdk + ndk { + version = AndroidConfig.ndk + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + isCoreLibraryDesugaringEnabled = true + } + + dependencies { + add("coreLibraryDesugaring", libs.desugar) + } + } + } +} + tasks.named("dependencyUpdates", com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask::class.java).configure { rejectVersionIf { val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { candidate.version.uppercase(Locale.ROOT).contains(it) } diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000000..02a4370ef1 --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("com.android.library") +} + +kotlin { + androidTarget() + sourceSets { + val commonMain by getting { + dependencies { + api(libs.okhttp) + api(libs.okhttp.logging.interceptor) + api(libs.okhttp.dnsoverhttps) + api(libs.okhttp.brotli) + api(libs.okio) + api(libs.bundles.logging) + } + } + val androidMain by getting { + dependencies { + api(androidx.core) + api(androidx.annotation) + api(libs.rxjava) + api(project.dependencies.enforcedPlatform(kotlinx.coroutines.bom)) + api(kotlinx.coroutines.core) + api(kotlinx.serialization.json) + api(kotlinx.serialization.json.okio) + + implementation(libs.quickjs.android) + } + } + } +} + +android { + namespace = "yokai.core" +} + +tasks { + withType { + kotlinOptions.freeCompilerArgs += listOf( + "-Xcontext-receivers", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + ) + } +} diff --git a/core/src/androidMain/AndroidManifest.xml b/core/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/core/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt index 03423e869d..c6f35435c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreference.kt @@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.core.preference import android.content.SharedPreferences import android.content.SharedPreferences.Editor +import android.os.Build +import androidx.annotation.RequiresApi import androidx.core.content.edit import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope @@ -152,6 +154,7 @@ sealed class AndroidPreference( } } + @RequiresApi(Build.VERSION_CODES.HONEYCOMB) class StringSetPrimitive( preferences: SharedPreferences, keyFlow: Flow, diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/core/preference/AndroidPreferenceStore.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/AndroidCookieJar.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/AndroidCookieJar.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/DohProviders.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/DohProviders.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/JavaScriptEngine.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/JavaScriptEngine.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/JavaScriptEngine.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt similarity index 68% rename from app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index b73ca6c50a..5fb1cd3703 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -1,10 +1,6 @@ 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 @@ -12,13 +8,14 @@ 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 +import java.util.concurrent.* -class NetworkHelper(val context: Context) { - - private val preferences: PreferencesHelper by injectLazy() +class NetworkHelper( + val context: Context, + private val networkPreferences: NetworkPreferences, + private val block: (OkHttpClient.Builder) -> Unit, +) { private val cacheDir = File(context.cacheDir, "network_cache") @@ -43,18 +40,9 @@ class NetworkHelper(val context: Context) { .addNetworkInterceptor(IgnoreGzipInterceptor()) .addNetworkInterceptor(BrotliInterceptor) .apply { - if (BuildConfig.DEBUG) { - addInterceptor( - ChuckerInterceptor.Builder(context) - .collector(ChuckerCollector(context)) - .maxContentLength(250000L) - .redactHeaders(emptySet()) - .alwaysReadResponseBody(false) - .build(), - ) - } + block(this) - when (preferences.dohProvider()) { + when (networkPreferences.dohProvider().get()) { PREF_DOH_CLOUDFLARE -> dohCloudflare() PREF_DOH_GOOGLE -> dohGoogle() PREF_DOH_ADGUARD -> dohAdGuard() @@ -75,7 +63,7 @@ class NetworkHelper(val context: Context) { } val defaultUserAgent - get() = preferences.defaultUserAgent().get().replace("\n", " ").trim() + get() = networkPreferences.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" diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt new file mode 100644 index 0000000000..fc122c9538 --- /dev/null +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/NetworkPreferences.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.network + +import android.os.Build +import androidx.annotation.RequiresApi +import eu.kanade.tachiyomi.core.preference.PreferenceStore + +class NetworkPreferences(private val preferenceStore: PreferenceStore) { + + fun dohProvider() = preferenceStore.getInt("doh_provider", -1) + + @RequiresApi(Build.VERSION_CODES.GINGERBREAD) + fun defaultUserAgent() = preferenceStore.getString("default_user_agent", NetworkHelper.DEFAULT_USER_AGENT) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt similarity index 99% rename from app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 875bbc2c73..2b7ea26e02 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -16,7 +16,7 @@ import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response import java.io.IOException -import java.util.concurrent.CountDownLatch +import java.util.concurrent.* class CloudflareInterceptor( private val context: Context, diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/IgnoreGzipInterceptor.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt index 994c3c032d..e059f2b742 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/RateLimitInterceptor.kt @@ -5,7 +5,7 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import java.io.IOException -import java.util.concurrent.TimeUnit +import java.util.concurrent.* /** * An OkHttp interceptor that handles rate limiting. diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt similarity index 98% rename from app/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt index b10b6904b5..a1307de1f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/SpecificHostRateLimitInterceptor.kt @@ -6,7 +6,7 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Response import java.io.IOException -import java.util.concurrent.TimeUnit +import java.util.concurrent.* /** * An OkHttp interceptor that handles given url host's rate limiting. diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UncaughtExceptionInterceptor.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index be8db9ee5e..1f366a189c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -15,9 +15,8 @@ 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 +import java.util.* +import java.util.concurrent.* abstract class WebViewInterceptor( private val context: Context, diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/CoroutinesExtensions.kt diff --git a/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt new file mode 100644 index 0000000000..6c04883652 --- /dev/null +++ b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/ToastExtensions.kt @@ -0,0 +1,25 @@ +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) { + Toast.makeText(this, resource, duration).show() +} + +/** + * 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) { + Toast.makeText(this, text.orEmpty(), duration).show() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewClientCompat.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt rename to core/src/androidMain/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/RxCoroutineBridge.kt b/core/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/util/system/RxCoroutineBridge.kt rename to core/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt index bba89d1d8e..a4909a77ba 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/RxCoroutineBridge.kt +++ b/core/src/androidMain/kotlin/yokai/util/lang/RxCoroutineBridge.kt @@ -1,17 +1,18 @@ -package eu.kanade.tachiyomi.util.system +package yokai.util.lang import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import rx.Emitter import rx.Observable import rx.Subscriber import rx.Subscription +import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -21,6 +22,7 @@ import kotlin.coroutines.resumeWithException suspend fun Observable.awaitSingle(): T = single().awaitOne() +@OptIn(InternalCoroutinesApi::class) private suspend fun Observable.awaitOne(): T = suspendCancellableCoroutine { cont -> cont.unsubscribeOnCancellation( subscribe( diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/Preference.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt similarity index 84% rename from app/src/main/java/eu/kanade/tachiyomi/core/preference/Preference.kt rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt index 6addefaa11..2950d0db48 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/core/preference/Preference.kt +++ b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/Preference.kt @@ -1,9 +1,5 @@ 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 @@ -77,9 +73,3 @@ fun Preference.toggle(): Boolean { set(!get()) return get() } - -@Composable -fun Preference.collectAsState(): State { - val flow = remember(this) { changes() } - return flow.collectAsState(initial = get()) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/core/preference/PreferenceStore.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt similarity index 99% rename from app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index e6eec02f4a..15c8839e5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -16,7 +16,7 @@ import rx.Observable import rx.Producer import rx.Subscription import java.io.IOException -import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.* import kotlin.coroutines.resumeWithException val jsonMime = "application/json; charset=utf-8".toMediaType() diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/ProgressListener.kt rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressListener.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/ProgressResponseBody.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt rename to core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt index 6adb0de8ef..dedc62fea3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/Requests.kt +++ b/core/src/commonMain/kotlin/eu/kanade/tachiyomi/network/Requests.kt @@ -7,7 +7,7 @@ import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.RequestBody -import java.util.concurrent.TimeUnit.MINUTES +import java.util.concurrent.TimeUnit.* private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build() private val DEFAULT_HEADERS = Headers.Builder().build() diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts new file mode 100644 index 0000000000..c35a38c744 --- /dev/null +++ b/domain/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "yokai.domain" +} + +dependencies { +} diff --git a/domain/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/domain/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt new file mode 100644 index 0000000000..20831e0228 --- /dev/null +++ b/domain/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -0,0 +1,384 @@ +package eu.kanade.tachiyomi.data.database.models + +import android.content.Context +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.updateStrategyAdapter +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.ui.reader.settings.OrientationType +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.* + +interface Manga : SManga { + + var id: Long? + + var source: Long + + var favorite: Boolean + + var last_update: Long + + var date_added: Long + + var viewer_flags: Int + + var chapter_flags: Int + + var hide_title: Boolean + + var filtered_scanlators: String? + + fun isBlank() = id == Long.MIN_VALUE + fun isHidden() = status == -1 + + fun setChapterOrder(sorting: Int, order: Int) { + setChapterFlags(sorting, CHAPTER_SORTING_MASK) + setChapterFlags(order, CHAPTER_SORT_MASK) + setChapterFlags(CHAPTER_SORT_LOCAL, CHAPTER_SORT_LOCAL_MASK) + } + + fun setSortToGlobal() = setChapterFlags(CHAPTER_SORT_FILTER_GLOBAL, CHAPTER_SORT_LOCAL_MASK) + + fun setFilterToGlobal() = setChapterFlags(CHAPTER_SORT_FILTER_GLOBAL, CHAPTER_FILTER_LOCAL_MASK) + fun setFilterToLocal() = setChapterFlags(CHAPTER_FILTER_LOCAL, CHAPTER_FILTER_LOCAL_MASK) + + private fun setChapterFlags(flag: Int, mask: Int) { + chapter_flags = chapter_flags and mask.inv() or (flag and mask) + } + + private fun setViewerFlags(flag: Int, mask: Int) { + viewer_flags = viewer_flags and mask.inv() or (flag and mask) + } + + val sortDescending: Boolean + get() = chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC + + val hideChapterTitles: Boolean + get() = displayMode == CHAPTER_DISPLAY_NUMBER + + val usesLocalSort: Boolean + get() = chapter_flags and CHAPTER_SORT_LOCAL_MASK == CHAPTER_SORT_LOCAL + + val usesLocalFilter: Boolean + get() = chapter_flags and CHAPTER_FILTER_LOCAL_MASK == CHAPTER_FILTER_LOCAL + + fun sortDescending(preferences: PreferencesHelper): Boolean = + if (usesLocalSort) sortDescending else preferences.chaptersDescAsDefault().get() + + fun chapterOrder(preferences: PreferencesHelper): Int = + if (usesLocalSort) sorting else preferences.sortChapterOrder().get() + + fun readFilter(preferences: PreferencesHelper): Int = + if (usesLocalFilter) readFilter else preferences.filterChapterByRead().get() + + fun downloadedFilter(preferences: PreferencesHelper): Int = + if (usesLocalFilter) downloadedFilter else preferences.filterChapterByDownloaded().get() + + fun bookmarkedFilter(preferences: PreferencesHelper): Int = + if (usesLocalFilter) bookmarkedFilter else preferences.filterChapterByBookmarked().get() + + fun hideChapterTitle(preferences: PreferencesHelper): Boolean = + if (usesLocalFilter) hideChapterTitles else preferences.hideChapterTitlesByDefault().get() + + fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and CHAPTER_DISPLAY_MASK == CHAPTER_DISPLAY_NUMBER + + fun seriesType(context: Context, sourceManager: SourceManager? = null): String { + return context.getString( + when (seriesType(sourceManager = sourceManager)) { + TYPE_WEBTOON -> R.string.webtoon + TYPE_MANHWA -> R.string.manhwa + TYPE_MANHUA -> R.string.manhua + TYPE_COMIC -> R.string.comic + else -> R.string.manga + }, + ).lowercase(Locale.getDefault()) + } + + fun getGenres(): List? { + return genre?.split(",") + ?.mapNotNull { tag -> tag.trim().takeUnless { it.isBlank() } } + } + + fun getOriginalGenres(): List? { + return (originalGenre ?: genre)?.split(",") + ?.mapNotNull { tag -> tag.trim().takeUnless { it.isBlank() } } + } + + /** + * The type of comic the manga is (ie. manga, manhwa, manhua) + */ + fun seriesType(useOriginalTags: Boolean = false, customTags: String? = null, sourceManager: SourceManager? = null): Int { + val sourceName by lazy { (sourceManager ?: Injekt.get()).getOrStub(source).name } + val tags = customTags ?: if (useOriginalTags) originalGenre else genre + val currentTags = tags?.split(",")?.map { it.trim().lowercase(Locale.US) } ?: emptyList() + return if (currentTags.any { tag -> isMangaTag(tag) }) { + TYPE_MANGA + } else if (currentTags.any { tag -> isComicTag(tag) } || + isComicSource(sourceName) + ) { + TYPE_COMIC + } else if (currentTags.any { tag -> isWebtoonTag(tag) } || + ( + sourceName.contains("webtoon", true) && + currentTags.none { tag -> isManhuaTag(tag) } && + currentTags.none { tag -> isManhwaTag(tag) } + ) + ) { + TYPE_WEBTOON + } else if (currentTags.any { tag -> isManhuaTag(tag) } || sourceName.contains( + "manhua", + true, + ) + ) { + TYPE_MANHUA + } else if (currentTags.any { tag -> isManhwaTag(tag) } || isWebtoonSource(sourceName)) { + TYPE_MANHWA + } else { + TYPE_MANGA + } + } + + /** + * The type the reader should use. Different from manga type as certain manga has different + * read types + */ + fun defaultReaderType(): Int { + val sourceName = Injekt.get().getOrStub(source).name + val currentTags = genre?.split(",")?.map { it.trim().lowercase(Locale.US) } ?: emptyList() + return if (currentTags.any + { tag -> + isManhwaTag(tag) || tag.contains("webtoon") + } || ( + isWebtoonSource(sourceName) && + currentTags.none { tag -> isManhuaTag(tag) } && + currentTags.none { tag -> isComicTag(tag) } + ) + ) { + ReadingModeType.LONG_STRIP.flagValue + } else if (currentTags.any + { tag -> + tag == "chinese" || tag == "manhua" || + tag.startsWith("english") || tag == "comic" + } || ( + isComicSource(sourceName) && !sourceName.contains("tapas", true) && + currentTags.none { tag -> isMangaTag(tag) } + ) || + (sourceName.contains("manhua", true) && currentTags.none { tag -> isMangaTag(tag) }) + ) { + ReadingModeType.LEFT_TO_RIGHT.flagValue + } else { + 0 + } + } + + fun isSeriesTag(tag: String): Boolean { + val tagLower = tag.lowercase(Locale.ROOT) + return isMangaTag(tagLower) || isManhuaTag(tagLower) || + isManhwaTag(tagLower) || isComicTag(tagLower) || isWebtoonTag(tagLower) + } + + fun isMangaTag(tag: String): Boolean { + return tag in listOf("manga", "манга", "jp") || tag.startsWith("japanese") + } + + fun isManhuaTag(tag: String): Boolean { + return tag in listOf("manhua", "маньхуа", "cn", "hk", "zh-Hans", "zh-Hant") || tag.startsWith("chinese") + } + + fun isLongStrip(): Boolean { + val currentTags = + genre?.split(",")?.map { it.trim().lowercase(Locale.US) } ?: emptyList() + return currentTags.any { it == "long strip" } + } + + fun isManhwaTag(tag: String): Boolean { + return tag in listOf("long strip", "manhwa", "манхва", "kr") || tag.startsWith("korean") + } + + fun isComicTag(tag: String): Boolean { + return tag in listOf("comic", "комикс", "en", "gb") || tag.startsWith("english") + } + + fun isWebtoonTag(tag: String): Boolean { + return tag.startsWith("webtoon") + } + + fun isWebtoonSource(sourceName: String): Boolean { + return sourceName.contains("webtoon", true) || + sourceName.contains("manhwa", true) || + sourceName.contains("toonily", true) + } + + fun isComicSource(sourceName: String): Boolean { + return sourceName.contains("gunnerkrigg", true) || + sourceName.contains("dilbert", true) || + sourceName.contains("cyanide", true) || + sourceName.contains("xkcd", true) || + sourceName.contains("tapas", true) || + sourceName.contains("ComicExtra", true) || + sourceName.contains("Read Comics Online", true) || + sourceName.contains("ReadComicOnline", true) + } + + fun isOneShotOrCompleted(db: DatabaseHelper): Boolean { + val tags by lazy { genre?.split(",")?.map { it.trim().lowercase(Locale.US) } } + val chapters by lazy { db.getChapters(this).executeAsBlocking() } + val firstChapterName by lazy { chapters.firstOrNull()?.name?.lowercase() ?: "" } + return status == SManga.COMPLETED || tags?.contains("oneshot") == true || + ( + chapters.size == 1 && + ( + Regex("one.?shot").containsMatchIn(firstChapterName) || + firstChapterName.contains("oneshot") + ) + ) + } + + fun key(): String { + return "manga-id-$id" + } + + // Used to display the chapter's title one way or another + var displayMode: Int + get() = chapter_flags and CHAPTER_DISPLAY_MASK + set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK) + + var readFilter: Int + get() = chapter_flags and CHAPTER_READ_MASK + set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK) + + var downloadedFilter: Int + get() = chapter_flags and CHAPTER_DOWNLOADED_MASK + set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK) + + var bookmarkedFilter: Int + get() = chapter_flags and CHAPTER_BOOKMARKED_MASK + set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK) + + var sorting: Int + get() = chapter_flags and CHAPTER_SORTING_MASK + set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK) + + var readingModeType: Int + get() = viewer_flags and ReadingModeType.MASK + set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK) + + var orientationType: Int + get() = viewer_flags and OrientationType.MASK + set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK) + + var vibrantCoverColor: Int? + get() = vibrantCoverColorMap[id] + set(value) { + id?.let { vibrantCoverColorMap[it] = value } + } + + var dominantCoverColors: Pair? + get() = MangaCoverMetadata.getColors(this) + set(value) { + value ?: return + MangaCoverMetadata.addCoverColor(this, value.first, value.second) + } + + companion object { + + // Generic filter that does not filter anything + const val SHOW_ALL = 0x00000000 + + const val CHAPTER_SORT_DESC = 0x00000000 + const val CHAPTER_SORT_ASC = 0x00000001 + const val CHAPTER_SORT_MASK = 0x00000001 + + const val CHAPTER_SORT_FILTER_GLOBAL = 0x00000000 + const val CHAPTER_SORT_LOCAL = 0x00001000 + const val CHAPTER_SORT_LOCAL_MASK = 0x00001000 + const val CHAPTER_FILTER_LOCAL = 0x00002000 + const val CHAPTER_FILTER_LOCAL_MASK = 0x00002000 + + const val CHAPTER_SHOW_UNREAD = 0x00000002 + const val CHAPTER_SHOW_READ = 0x00000004 + const val CHAPTER_READ_MASK = 0x00000006 + + const val CHAPTER_SHOW_DOWNLOADED = 0x00000008 + const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010 + const val CHAPTER_DOWNLOADED_MASK = 0x00000018 + + const val CHAPTER_SHOW_BOOKMARKED = 0x00000020 + const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040 + const val CHAPTER_BOOKMARKED_MASK = 0x00000060 + + const val CHAPTER_SORTING_SOURCE = 0x00000000 + const val CHAPTER_SORTING_NUMBER = 0x00000100 + const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200 + const val CHAPTER_SORTING_MASK = 0x00000300 + + const val CHAPTER_DISPLAY_NAME = 0x00000000 + const val CHAPTER_DISPLAY_NUMBER = 0x00100000 + const val CHAPTER_DISPLAY_MASK = 0x00100000 + + const val TYPE_MANGA = 1 + const val TYPE_MANHWA = 2 + const val TYPE_MANHUA = 3 + const val TYPE_COMIC = 4 + const val TYPE_WEBTOON = 5 + + private val vibrantCoverColorMap: HashMap = hashMapOf() + + fun create(source: Long): Manga = MangaImpl().apply { + this.source = source + } + + fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply { + url = pathUrl + this.title = title + this.source = source + } + + fun mapper( + id: Long, + source: Long, + url: String, + artist: String?, + author: String?, + description: String?, + genre: String?, + title: String, + status: Long, + thumbnailUrl: String?, + favorite: Long, + lastUpdate: Long?, + initialized: Boolean, + viewerFlags: Long, + hideTitle: Long, + chapterFlags: Long, + dateAdded: Long?, + filteredScanlators: String?, + updateStrategy: Long + ): Manga = create(source).apply { + this.id = id + this.url = url + this.artist = artist + this.author = author + this.description = description + this.genre = genre + this.title = title + this.status = status.toInt() + this.thumbnail_url = thumbnailUrl + this.favorite = favorite > 0 + this.last_update = lastUpdate ?: 0L + this.initialized = initialized + this.viewer_flags = viewerFlags.toInt() + this.chapter_flags = chapterFlags.toInt() + this.hide_title = hideTitle > 0 + this.date_added = dateAdded ?: 0L + this.filtered_scanlators = filteredScanlators + this.update_strategy = updateStrategy.toInt().let(updateStrategyAdapter::decode) + } + } +} diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index b6ef6bf683..8c4afaf538 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,13 +1,13 @@ [versions] kotlin = "1.9.24" -coroutines = "1.8.0" serialization = "1.6.2" xml_serialization = "0.86.3" [libraries] -coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } -coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } -coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version = "1.8.0" } +coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android" } +coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } +coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" } serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" } @@ -22,6 +22,7 @@ serialization = [ "serialization-gradle", "serialization-json", "serialization-json-okio", "serialization-protobuf", "serialization-xml", "serialization-xml-core" ] +coroutines = [ "coroutines-android", "coroutines-core" ] [plugins] android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/presentation-core/build.gradle.kts b/presentation-core/build.gradle.kts new file mode 100644 index 0000000000..7c080dcdb5 --- /dev/null +++ b/presentation-core/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "yokai.presentation.core" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } +} + +dependencies { + +} diff --git a/presentation-core/consumer-rules.pro b/presentation-core/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/presentation-core/proguard-rules.pro b/presentation-core/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/presentation-core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/presentation-widget/build.gradle.kts b/presentation-widget/build.gradle.kts new file mode 100644 index 0000000000..9f0c945ef1 --- /dev/null +++ b/presentation-widget/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "yokai.presentation.widget" + + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = compose.versions.compose.compiler.get() + } +} + +dependencies { + implementation(project(":core")) + implementation(project(":domain")) + implementation(project(":presentation-core")) + + implementation(androidx.glance.appwidget) + + implementation(libs.coil3) +} diff --git a/presentation-widget/consumer-rules.pro b/presentation-widget/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/presentation-widget/proguard-rules.pro b/presentation-widget/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/presentation-widget/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/presentation-widget/src/main/java/TachiyomiWidgetManager.kt b/presentation-widget/src/main/java/TachiyomiWidgetManager.kt new file mode 100644 index 0000000000..beb63f41bf --- /dev/null +++ b/presentation-widget/src/main/java/TachiyomiWidgetManager.kt @@ -0,0 +1,14 @@ +package yokai.presentation.widget + +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidgetManager + +class TachiyomiWidgetManager { + + suspend fun Context.init() { + val manager = GlanceAppWidgetManager(this) + if (manager.getGlanceIds(UpdatesGridGlanceWidget::class.java).isNotEmpty()) { + UpdatesGridGlanceWidget().loadData() + } + } +} diff --git a/presentation-widget/src/main/java/UpdatesGridGlanceReceiver.kt b/presentation-widget/src/main/java/UpdatesGridGlanceReceiver.kt new file mode 100644 index 0000000000..b262ec5f35 --- /dev/null +++ b/presentation-widget/src/main/java/UpdatesGridGlanceReceiver.kt @@ -0,0 +1,8 @@ +package yokai.presentation.widget + +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetReceiver + +class UpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() { + override val glanceAppWidget: GlanceAppWidget = UpdatesGridGlanceWidget().apply { loadData() } +} diff --git a/presentation-widget/src/main/java/UpdatesGridGlanceWidget.kt b/presentation-widget/src/main/java/UpdatesGridGlanceWidget.kt new file mode 100644 index 0000000000..d839cb14bf --- /dev/null +++ b/presentation-widget/src/main/java/UpdatesGridGlanceWidget.kt @@ -0,0 +1,128 @@ +package yokai.presentation.widget + +import android.app.Application +import android.content.Context +import android.graphics.Bitmap +import android.os.Build +import androidx.core.graphics.drawable.toBitmap +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.ImageProvider +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.SizeMode +import androidx.glance.appwidget.appWidgetBackground +import androidx.glance.appwidget.provideContent +import androidx.glance.appwidget.updateAll +import androidx.glance.background +import androidx.glance.layout.fillMaxSize +import coil3.executeBlocking +import coil3.imageLoader +import coil3.request.CachePolicy +import coil3.request.ImageRequest +import coil3.request.transformations +import coil3.size.Precision +import coil3.size.Scale +import coil3.transform.RoundedCornersTransformation +import eu.kanade.tachiyomi.appwidget.components.CoverHeight +import eu.kanade.tachiyomi.appwidget.components.CoverWidth +import eu.kanade.tachiyomi.appwidget.components.LockedWidget +import eu.kanade.tachiyomi.appwidget.components.UpdatesWidget +import eu.kanade.tachiyomi.appwidget.util.appWidgetBackgroundRadius +import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.recents.RecentsPresenter +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.launchIO +import kotlinx.coroutines.MainScope +import uy.kohesive.injekt.injectLazy +import java.util.* +import kotlin.math.min + +class UpdatesGridGlanceWidget : GlanceAppWidget() { + private val app: Application by injectLazy() + private val preferences: PreferencesHelper by injectLazy() + + private val coroutineScope = MainScope() + + private var data: List>? = null + + override val sizeMode = SizeMode.Exact + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { + // If app lock enabled, don't do anything + if (preferences.useBiometrics().get()) { + LockedWidget() + } else { + UpdatesWidget(data) + } + } + } + + fun loadData(list: List>? = null) { + coroutineScope.launchIO { + // Don't show anything when lock is active + if (preferences.useBiometrics().get()) { + updateAll(app) + return@launchIO + } + + val manager = GlanceAppWidgetManager(app) + val ids = manager.getGlanceIds(this@UpdatesGridGlanceWidget::class.java) + if (ids.isEmpty()) return@launchIO + + val (rowCount, columnCount) = ids + .flatMap { manager.getAppWidgetSizes(it) } + .maxBy { it.height.value * it.width.value } + .calculateRowAndColumnCount() + val processList = list ?: RecentsPresenter.getRecentManga(customAmount = min(50, rowCount * columnCount)) + + data = prepareList(processList, rowCount * columnCount) + ids.forEach { update(app, it) } + } + } + + private fun prepareList(processList: List>, take: Int): List> { + // Resize to cover size + val widthPx = CoverWidth.value.toInt().dpToPx + val heightPx = CoverHeight.value.toInt().dpToPx + val roundPx = app.resources.getDimension(R.dimen.appwidget_inner_radius) + return processList +// .distinctBy { it.first.id } + .sortedByDescending { it.second } + .take(take) + .map { it.first } + .map { updatesView -> + val request = ImageRequest.Builder(app) + .data(updatesView) + .memoryCachePolicy(CachePolicy.DISABLED) + .precision(Precision.EXACT) + .size(widthPx, heightPx) + .scale(Scale.FILL) + .let { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + it.transformations(RoundedCornersTransformation(roundPx)) + } else { + it // Handled by system + } + } + .build() + Pair(updatesView.id!!, app.imageLoader.executeBlocking(request).image?.asDrawable(app.resources)?.toBitmap()) + } + } + + companion object { + val DateLimit: Calendar + get() = Calendar.getInstance().apply { + time = Date() + add(Calendar.MONTH, -3) + } + } +} + +val ContainerModifier = GlanceModifier + .fillMaxSize() + .background(ImageProvider(R.drawable.appwidget_background)) + .appWidgetBackground() + .appWidgetBackgroundRadius() diff --git a/presentation-widget/src/main/java/components/LockedWidget.kt b/presentation-widget/src/main/java/components/LockedWidget.kt new file mode 100644 index 0000000000..07471d8521 --- /dev/null +++ b/presentation-widget/src/main/java/components/LockedWidget.kt @@ -0,0 +1,44 @@ +package yokai.presentation.widget.components + +import android.content.Intent +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceModifier +import androidx.glance.LocalContext +import androidx.glance.action.clickable +import androidx.glance.appwidget.action.actionStartActivity +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.padding +import androidx.glance.text.Text +import androidx.glance.text.TextAlign +import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.appwidget.ContainerModifier +import eu.kanade.tachiyomi.appwidget.util.stringResource +import eu.kanade.tachiyomi.ui.main.MainActivity + +@Composable +fun LockedWidget() { + val intent = Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + Box( + modifier = GlanceModifier + .clickable(actionStartActivity(intent)) + .then(ContainerModifier) + .padding(8.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResource(R.string.appwidget_unavailable_locked), + style = TextStyle( + color = ColorProvider(R.color.appwidget_on_secondary_container), + fontSize = 12.sp, + textAlign = TextAlign.Center, + ), + ) + } +} diff --git a/presentation-widget/src/main/java/components/UpdatesMangaCover.kt b/presentation-widget/src/main/java/components/UpdatesMangaCover.kt new file mode 100644 index 0000000000..def7f8db65 --- /dev/null +++ b/presentation-widget/src/main/java/components/UpdatesMangaCover.kt @@ -0,0 +1,48 @@ +package yokai.presentation.widget.components + +import android.graphics.Bitmap +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.ImageProvider +import androidx.glance.layout.Box +import androidx.glance.layout.ContentScale +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.size +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.appwidget.util.appWidgetInnerRadius + +val CoverWidth = 58.dp +val CoverHeight = 87.dp + +@Composable +fun UpdatesMangaCover( + modifier: GlanceModifier = GlanceModifier, + cover: Bitmap?, +) { + Box( + modifier = modifier + .size(width = CoverWidth, height = CoverHeight) + .appWidgetInnerRadius(), + ) { + if (cover != null) { + Image( + provider = ImageProvider(cover), + contentDescription = null, + modifier = GlanceModifier + .fillMaxSize() + .appWidgetInnerRadius(), + contentScale = ContentScale.Crop, + ) + } else { + // Enjoy placeholder + Image( + provider = ImageProvider(R.drawable.appwidget_cover_error), + contentDescription = null, + modifier = GlanceModifier.fillMaxSize(), + contentScale = ContentScale.Crop, + ) + } + } +} diff --git a/presentation-widget/src/main/java/components/UpdatesWidget.kt b/presentation-widget/src/main/java/components/UpdatesWidget.kt new file mode 100644 index 0000000000..906c8f09cb --- /dev/null +++ b/presentation-widget/src/main/java/components/UpdatesWidget.kt @@ -0,0 +1,75 @@ +package yokai.presentation.widget.components + +import android.content.Intent +import android.graphics.Bitmap +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import androidx.glance.GlanceModifier +import androidx.glance.LocalContext +import androidx.glance.LocalSize +import androidx.glance.action.clickable +import androidx.glance.appwidget.CircularProgressIndicator +import androidx.glance.appwidget.action.actionStartActivity +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.Row +import androidx.glance.layout.fillMaxWidth +import androidx.glance.layout.padding +import androidx.glance.text.Text +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.appwidget.ContainerModifier +import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount +import eu.kanade.tachiyomi.appwidget.util.stringResource +import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.main.SearchActivity + +@Composable +fun UpdatesWidget(data: List>?) { + val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount() + val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + Column( + modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)), + verticalAlignment = Alignment.CenterVertically, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (data == null) { + CircularProgressIndicator() + } else if (data.isEmpty()) { + Text(text = stringResource(R.string.no_recent_read_updated_manga)) + } else { + (0 until rowCount).forEach { i -> + val coverRow = (0 until columnCount).mapNotNull { j -> + data.getOrNull(j + (i * columnCount)) + } + if (coverRow.isNotEmpty()) { + Row( + modifier = GlanceModifier + .padding(vertical = 4.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalAlignment = Alignment.CenterVertically, + ) { + coverRow.forEach { (mangaId, cover) -> + Box( + modifier = GlanceModifier + .padding(horizontal = 3.dp), + contentAlignment = Alignment.Center, + ) { + val intent = SearchActivity.openMangaIntent(LocalContext.current, mangaId, true) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + // https://issuetracker.google.com/issues/238793260 + .addCategory(mangaId.toString()) + UpdatesMangaCover( + modifier = GlanceModifier.clickable(actionStartActivity(intent)), + cover = cover, + ) + } + } + } + } + } + } + } +} diff --git a/presentation-widget/src/main/java/util/GlanceUtils.kt b/presentation-widget/src/main/java/util/GlanceUtils.kt new file mode 100644 index 0000000000..81214717e4 --- /dev/null +++ b/presentation-widget/src/main/java/util/GlanceUtils.kt @@ -0,0 +1,43 @@ +package yokai.presentation.widget.util + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.DpSize +import androidx.glance.GlanceModifier +import androidx.glance.LocalContext +import androidx.glance.appwidget.cornerRadius +import eu.kanade.tachiyomi.R + +fun GlanceModifier.appWidgetBackgroundRadius(): GlanceModifier { + return this.cornerRadius(R.dimen.appwidget_background_radius) +} + +fun GlanceModifier.appWidgetInnerRadius(): GlanceModifier { + return this.cornerRadius(R.dimen.appwidget_inner_radius) +} + +@Composable +fun stringResource(@StringRes id: Int): String { + return LocalContext.current.getString(id) +} + +/** + * Calculates row-column count. + * + * Row + * Numerator: Container height - container vertical padding + * Denominator: Cover height + cover vertical padding + * + * Column + * Numerator: Container width - container horizontal padding + * Denominator: Cover width + cover horizontal padding + * + * @return pair of row and column count + */ +fun DpSize.calculateRowAndColumnCount(): Pair { + // Hack: Size provided by Glance manager is not reliable so take at least 1 row and 1 column + // Set max to 10 children each direction because of Glance limitation + val rowCount = (height.value / 95).toInt().coerceIn(1, 10) + val columnCount = (width.value / 64).toInt().coerceIn(1, 10) + return Pair(rowCount, columnCount) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c0a5987077..72192d1c8b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,5 +27,12 @@ dependencyResolutionManagement { } } +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + rootProject.name = "Yokai" include(":app") +include(":core") +include(":domain") +include(":presentation-core") +include(":presentation-widget") +include(":source-api") diff --git a/source-api/build.gradle.kts b/source-api/build.gradle.kts new file mode 100644 index 0000000000..0cbb0380dc --- /dev/null +++ b/source-api/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + id("com.android.library") +} + +kotlin { + androidTarget() + sourceSets { + val commonMain by getting { + dependencies { + api(kotlinx.serialization.json) + api(libs.injekt.core) + api(libs.rxjava) + api(libs.jsoup) + } + } + val androidMain by getting { + dependencies { + implementation(projects.core) + api(androidx.preference) + + // Workaround for https://youtrack.jetbrains.com/issue/KT-57605 + implementation(kotlinx.coroutines.android) + implementation(project.dependencies.platform(kotlinx.coroutines.bom)) + } + } + } +} +android { + namespace = "eu.kanade.tachiyomi.source" + defaultConfig { + consumerProguardFile("consumer-proguard.pro") + } +} +tasks { + withType { + kotlinOptions.freeCompilerArgs += listOf( + "-Xexpect-actual-classes", + ) + } +} diff --git a/source-api/consumer-proguard.pro b/source-api/consumer-proguard.pro new file mode 100644 index 0000000000..aa81da4bfd --- /dev/null +++ b/source-api/consumer-proguard.pro @@ -0,0 +1,5 @@ +-keep class eu.kanade.tachiyomi.source.model.** { public protected *; } +-keep class eu.kanade.tachiyomi.source.online.** { public protected *; } +-keep class eu.kanade.tachiyomi.source.** extends eu.kanade.tachiyomi.source.Source { public protected *; } + +-keep,allowoptimization class eu.kanade.tachiyomi.util.JsoupExtensionsKt { public protected *; } diff --git a/source-api/src/androidMain/AndroidManifest.xml b/source-api/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..8072ee00db --- /dev/null +++ b/source-api/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/source-api/src/androidMain/kotlin/eu/kanade/tachiyomi/util/RxExtension.kt b/source-api/src/androidMain/kotlin/eu/kanade/tachiyomi/util/RxExtension.kt new file mode 100644 index 0000000000..9a76345cb9 --- /dev/null +++ b/source-api/src/androidMain/kotlin/eu/kanade/tachiyomi/util/RxExtension.kt @@ -0,0 +1,6 @@ +package eu.kanade.tachiyomi.util + +import rx.Observable +import yokai.util.lang.awaitSingle + +actual suspend fun Observable.awaitSingle(): T = awaitSingle() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt index 01cc4a4fc9..f65c2662a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/CatalogueSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/CatalogueSource.kt @@ -2,7 +2,7 @@ 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 eu.kanade.tachiyomi.util.awaitSingle import rx.Observable interface CatalogueSource : Source { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/ConfigurableSource.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/ConfigurableSource.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt similarity index 67% rename from app/src/main/java/eu/kanade/tachiyomi/source/Source.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt index b2167ee94c..fec2f89674 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/Source.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/Source.kt @@ -1,15 +1,10 @@ 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 eu.kanade.tachiyomi.util.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. @@ -66,19 +61,6 @@ interface Source { return fetchPageList(chapter).awaitSingle() } - fun includeLangInName(enabledLanguages: Set, 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, extensionManager: ExtensionManager? = null): String { - return if (includeLangInName(enabledLanguages, extensionManager)) toString() else name - } - @Deprecated( "Use the non-RxJava API instead", ReplaceWith("getMangaDetails"), @@ -101,6 +83,4 @@ interface Source { throw IllegalStateException("Not used") } -fun Source.icon(): Drawable? = Injekt.get().getAppIconForSource(this) - fun Source.preferenceKey(): String = "source_$id" diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceFactory.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/SourceFactory.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/SourceFactory.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/SourceFactory.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/UnmeteredSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/UnmeteredSource.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/UnmeteredSource.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/UnmeteredSource.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/model/Filter.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/FilterList.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/model/FilterList.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/FilterList.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/MangasPage.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/model/MangasPage.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/MangasPage.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Page.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapter.kt similarity index 59% rename from app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapter.kt index 756b8f2c84..f53bbe8f0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapter.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapter.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.source.model -import eu.kanade.tachiyomi.data.database.models.ChapterImpl import java.io.Serializable interface SChapter : Serializable { @@ -23,16 +22,6 @@ interface SChapter : Serializable { 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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapterImpl.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/model/SChapterImpl.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SChapterImpl.kt diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt new file mode 100644 index 0000000000..c0f8ef8281 --- /dev/null +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt @@ -0,0 +1,52 @@ +package eu.kanade.tachiyomi.source.model + +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 + + 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 SMangaImpl() + } + } +} diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaImpl.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaImpl.kt new file mode 100644 index 0000000000..91a7711cce --- /dev/null +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/SMangaImpl.kt @@ -0,0 +1,24 @@ +package eu.kanade.tachiyomi.source.model + +class SMangaImpl : SManga { + + override lateinit var url: String + + override lateinit var title: String + + override var artist: String? = null + + override var author: String? = null + + override var description: String? = null + + override var genre: String? = null + + override var status: Int = 0 + + override var thumbnail_url: String? = null + + override var update_strategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE + + override var initialized: Boolean = false +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/UpdateStrategy.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt similarity index 96% rename from app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt index 5fc6ba155b..2850ae2c18 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -1,7 +1,5 @@ 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 @@ -13,15 +11,12 @@ 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 eu.kanade.tachiyomi.util.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 @@ -121,12 +116,6 @@ abstract class HttpSource : CatalogueSource { } } - 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. * @@ -509,6 +498,22 @@ abstract class HttpSource : CatalogueSource { * Returns the list of filters for the source. */ override fun getFilterList() = FilterList() + + private fun String.getUrlWithoutDomain(): String { + return try { + val uri = URI(this.replace(" ", "%20")) + var out = uri.path + if (uri.query != null) { + out += "?" + uri.query + } + if (uri.fragment != null) { + out += "#" + uri.fragment + } + out + } catch (e: URISyntaxException) { + this + } + } } class LicensedMangaChaptersException : Exception("Licensed - No chapters to show") diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt similarity index 100% rename from app/src/main/java/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt rename to source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/online/ParsedHttpSource.kt diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/JsoupExtensions.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/JsoupExtensions.kt new file mode 100644 index 0000000000..6c166448a5 --- /dev/null +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/JsoupExtensions.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.util + +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +fun Element.selectText(css: String, defaultValue: String? = null): String? { + return select(css).first()?.text() ?: defaultValue +} + +fun Element.selectInt(css: String, defaultValue: Int = 0): Int { + return select(css).first()?.text()?.toInt() ?: defaultValue +} + +fun Element.attrOrText(css: String): String { + return if (css != "text") attr(css) else text() +} + +/** + * Returns a Jsoup document for this response. + * @param html the body of the response. Use only if the body was read before calling this method. + */ +fun Response.asJsoup(html: String? = null): Document { + return Jsoup.parse(html ?: body.string(), request.url.toString()) +} diff --git a/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/RxExtension.kt b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/RxExtension.kt new file mode 100644 index 0000000000..5b8420d412 --- /dev/null +++ b/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/util/RxExtension.kt @@ -0,0 +1,5 @@ +package eu.kanade.tachiyomi.util + +import rx.Observable + +expect suspend fun Observable.awaitSingle(): T