mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 02:34:39 +00:00
refactor: Modularize the project (#97)
* refactor: Modularize the project * chore: Move okhttp stuff back to androidMain OkHttp decided to cancel multiplatform plan on 5.0 REF: https://square.github.io/okhttp/changelogs/changelog/#version-500-alpha13 * feat: Start using moko for i18n * fix: Solve some errors * chore: Remove manga from domain module We'll do this later * fix: Duplicate error * fix: Conflict function name error * fix: Target SManga * fix: Breaking changes after the split * fix: Not enough heap memory * chore: Update proguard rules Sorta similar to upstream * refactor: Fix namespace
This commit is contained in:
parent
7e7a37bc53
commit
24ce2683d4
109 changed files with 1146 additions and 308 deletions
|
@ -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,10 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core)
|
||||
implementation(projects.i18n)
|
||||
implementation(projects.sourceApi)
|
||||
|
||||
// Compose
|
||||
implementation(compose.bundles.compose)
|
||||
debugImplementation(compose.ui.tooling)
|
||||
|
@ -280,8 +271,8 @@ dependencies {
|
|||
|
||||
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
|
||||
|
||||
implementation(kotlinx.coroutines.core)
|
||||
implementation(kotlinx.coroutines.android)
|
||||
implementation(platform(kotlinx.coroutines.bom))
|
||||
implementation(kotlinx.bundles.coroutines)
|
||||
|
||||
// Text distance
|
||||
implementation(libs.java.string.similarity)
|
||||
|
@ -296,8 +287,6 @@ dependencies {
|
|||
|
||||
implementation(kotlinx.immutable)
|
||||
|
||||
"coreLibraryDesugaring"(libs.desugar)
|
||||
|
||||
// Tests
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.bundles.test.runtime)
|
||||
|
@ -306,13 +295,6 @@ dependencies {
|
|||
}
|
||||
|
||||
tasks {
|
||||
withType<Test> {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events("passed", "skipped", "failed")
|
||||
}
|
||||
}
|
||||
|
||||
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
|
||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
|
|
13
app/proguard-rules.pro
vendored
13
app/proguard-rules.pro
vendored
|
@ -1,19 +1,20 @@
|
|||
-dontobfuscate
|
||||
|
||||
-keep class eu.kanade.tachiyomi.source.** { public protected *; } # Avoid access modification
|
||||
-keep,allowoptimization class eu.kanade.** { public protected *; }
|
||||
-keep,allowoptimization class tachiyomi.** { public protected *; }
|
||||
-keep,allowoptimization class dev.yokai.** { public protected *; }
|
||||
-keep,allowoptimization class yokai.** { public protected *; }
|
||||
|
||||
# Keep common dependencies used in extensions
|
||||
-keep class androidx.preference.** { public protected *; }
|
||||
-keep class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class androidx.preference.** { public protected *; }
|
||||
-keep,allowoptimization class kotlin.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.coroutines.** { public protected *; }
|
||||
-keep class kotlinx.serialization.** { public protected *; }
|
||||
-keep class okhttp3.** { public protected *; }
|
||||
-keep,allowoptimization class kotlinx.serialization.** { public protected *; }
|
||||
-keep,allowoptimization class kotlin.time.** { public protected *; }
|
||||
-keep,allowoptimization class okhttp3.** { public protected *; }
|
||||
-keep,allowoptimization class okio.** { public protected *; }
|
||||
-keep,allowoptimization class rx.** { public protected *; }
|
||||
-keep class org.jsoup.** { public protected *; }
|
||||
-keep,allowoptimization class org.jsoup.** { public protected *; }
|
||||
-keep,allowoptimization class com.google.gson.** { public protected *; }
|
||||
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
|
||||
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -36,4 +36,5 @@ val migrations: ImmutableList<Migration> = persistentListOf(
|
|||
ExtensionInstallerEnumMigration(),
|
||||
CutoutMigration(),
|
||||
RepoJsonMigration(),
|
||||
NetworkPrefsMigration(),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.core.net.toUri
|
||||
import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.core.storage.preference.collectAsState
|
||||
|
||||
@Composable
|
||||
fun storageLocationText(
|
||||
|
|
|
@ -20,7 +20,7 @@ import dev.yokai.presentation.component.preference.widget.SliderPreferenceWidget
|
|||
import dev.yokai.presentation.component.preference.widget.SwitchPreferenceWidget
|
||||
import dev.yokai.presentation.component.preference.widget.TextPreferenceWidget
|
||||
import dev.yokai.presentation.component.preference.widget.TrackingPreferenceWidget
|
||||
import eu.kanade.tachiyomi.core.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.core.storage.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.data.track.TrackPreferences
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
|
|
@ -4,7 +4,7 @@ import androidx.activity.compose.BackHandler
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import dev.yokai.domain.base.BasePreferences
|
||||
import eu.kanade.tachiyomi.core.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.core.storage.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
|
|
@ -22,14 +22,12 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.ActivityCompat
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import dev.yokai.presentation.component.ThemeItem
|
||||
import dev.yokai.presentation.theme.Size
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.core.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.core.storage.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.system.Themes
|
||||
import eu.kanade.tachiyomi.util.system.appDelegateNightMode
|
||||
|
|
|
@ -21,7 +21,7 @@ import dev.yokai.presentation.component.Gap
|
|||
import dev.yokai.presentation.component.preference.Preference
|
||||
import dev.yokai.presentation.component.preference.PreferenceItem
|
||||
import dev.yokai.presentation.component.preference.widget.PreferenceGroupHeader
|
||||
import eu.kanade.tachiyomi.core.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.core.storage.preference.collectAsState
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.compose.LocalAlertDialog
|
||||
import eu.kanade.tachiyomi.util.compose.LocalBackPress
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package eu.kanade.tachiyomi.core.storage.preference
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
|
||||
@Composable
|
||||
fun <T> Preference<T>.collectAsState(): State<T> {
|
||||
val flow = remember(this) { changes() }
|
||||
return flow.collectAsState(initial = get())
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.ui.reader.settings.ReadingModeType
|
|||
import eu.kanade.tachiyomi.util.manga.MangaCoverMetadata
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
interface Manga : SManga {
|
||||
|
||||
|
@ -34,6 +34,55 @@ interface Manga : SManga {
|
|||
|
||||
var filtered_scanlators: String?
|
||||
|
||||
val originalTitle: String
|
||||
get() = (this as? MangaImpl)?.ogTitle ?: title
|
||||
val originalAuthor: String?
|
||||
get() = (this as? MangaImpl)?.ogAuthor ?: author
|
||||
val originalArtist: String?
|
||||
get() = (this as? MangaImpl)?.ogArtist ?: artist
|
||||
val originalDescription: String?
|
||||
get() = (this as? MangaImpl)?.ogDesc ?: description
|
||||
val originalGenre: String?
|
||||
get() = (this as? MangaImpl)?.ogGenre ?: genre
|
||||
val originalStatus: Int
|
||||
get() = (this as? MangaImpl)?.ogStatus ?: status
|
||||
|
||||
val hasSameAuthorAndArtist: Boolean
|
||||
get() = author == artist || artist.isNullOrBlank() ||
|
||||
author?.contains(artist ?: "", true) == true
|
||||
|
||||
fun copyFrom(other: SManga) {
|
||||
if (other is Manga) {
|
||||
if (other.author != null) {
|
||||
author = other.originalAuthor
|
||||
}
|
||||
|
||||
if (other.artist != null) {
|
||||
artist = other.originalArtist
|
||||
}
|
||||
|
||||
if (other.description != null) {
|
||||
description = other.originalDescription
|
||||
}
|
||||
|
||||
if (other.genre != null) {
|
||||
genre = other.originalGenre
|
||||
}
|
||||
|
||||
if (other.thumbnail_url != null) {
|
||||
thumbnail_url = other.thumbnail_url
|
||||
}
|
||||
|
||||
status = other.originalStatus
|
||||
}
|
||||
|
||||
update_strategy = other.update_strategy
|
||||
|
||||
if (!initialized) {
|
||||
initialized = other.initialized
|
||||
}
|
||||
}
|
||||
|
||||
fun isBlank() = id == Long.MIN_VALUE
|
||||
fun isHidden() = status == -1
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
fun Source.includeLangInName(enabledLanguages: Set<String>, extensionManager: ExtensionManager? = null): Boolean {
|
||||
val httpSource = this as? HttpSource ?: return true
|
||||
val extManager = extensionManager ?: Injekt.get()
|
||||
val allExt = httpSource.getExtension(extManager)?.lang == "all"
|
||||
val onlyAll = httpSource.extOnlyHasAllLanguage(extManager)
|
||||
val isMultiLingual = enabledLanguages.filterNot { it == "all" }.size > 1
|
||||
return (isMultiLingual && allExt) || (lang == "all" && !onlyAll)
|
||||
}
|
||||
|
||||
fun Source.nameBasedOnEnabledLanguages(enabledLanguages: Set<String>, extensionManager: ExtensionManager? = null): String {
|
||||
return if (includeLangInName(enabledLanguages, extensionManager)) toString() else name
|
||||
}
|
||||
|
||||
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
fun HttpSource.getExtension(extensionManager: ExtensionManager? = null): Extension.Installed? =
|
||||
(extensionManager ?: Injekt.get()).installedExtensionsFlow.value.find { it.sources.contains(this) }
|
||||
|
||||
fun HttpSource.extOnlyHasAllLanguage(extensionManager: ExtensionManager? = null) =
|
||||
getExtension(extensionManager)?.sources?.all { it.lang == "all" } ?: true
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package eu.kanade.tachiyomi.source.models
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
|
||||
val SManga.originalTitle: String
|
||||
get() = if (this is Manga) this.originalTitle else title
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.SourceNotFoundException
|
||||
import eu.kanade.tachiyomi.source.getExtension
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
|
||||
|
@ -77,8 +78,7 @@ import uy.kohesive.injekt.injectLazy
|
|||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
class MangaDetailsPresenter(
|
||||
val manga: Manga,
|
||||
|
|
|
@ -26,17 +26,18 @@ import androidx.core.widget.TextViewCompat
|
|||
import androidx.transition.TransitionSet
|
||||
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import coil3.request.CachePolicy
|
||||
import coil3.request.placeholder
|
||||
import coil3.request.error
|
||||
import coil3.request.placeholder
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.chip.Chip
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.coil.loadManga
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.databinding.ChapterHeaderItemBinding
|
||||
import eu.kanade.tachiyomi.databinding.MangaHeaderItemBinding
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
import eu.kanade.tachiyomi.util.lang.toNormalized
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
|
|||
import android.view.View
|
||||
import eu.kanade.tachiyomi.databinding.MigrationSourceItemBinding
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.database.models.Category
|
|||
import eu.kanade.tachiyomi.databinding.StatsDetailsChartBinding
|
||||
import eu.kanade.tachiyomi.databinding.StatsDetailsControllerBinding
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
|
||||
import eu.kanade.tachiyomi.ui.base.SmallToolbarInterface
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseCoroutineController
|
||||
import eu.kanade.tachiyomi.ui.library.FilteredLibraryController
|
||||
|
@ -59,8 +60,7 @@ import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
|||
import eu.kanade.tachiyomi.util.view.withFadeTransaction
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.*
|
||||
|
||||
class StatsDetailsController :
|
||||
BaseCoroutineController<StatsDetailsControllerBinding, StatsDetailsPresenter>(),
|
||||
|
|
|
@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.source.LocalSource
|
|||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.source.nameBasedOnEnabledLanguages
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BaseCoroutinePresenter
|
||||
import eu.kanade.tachiyomi.ui.more.stats.StatsHelper
|
||||
import eu.kanade.tachiyomi.util.isLocal
|
||||
|
@ -32,9 +33,8 @@ import kotlinx.coroutines.runBlocking
|
|||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class StatsDetailsPresenter(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||
import eu.kanade.tachiyomi.databinding.SourceItemBinding
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.icon
|
||||
import eu.kanade.tachiyomi.source.includeLangInName
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.view.compatToolTipText
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.util.chapter
|
|||
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.models.originalTitle
|
||||
|
||||
/**
|
||||
* -R> = regex conversion.
|
||||
|
|
|
@ -17,14 +17,11 @@ import android.net.wifi.WifiManager
|
|||
import android.os.Build
|
||||
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 +51,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.
|
||||
*
|
||||
|
@ -125,30 +102,6 @@ fun Context.contextCompatDrawable(@DrawableRes resource: Int): Drawable? {
|
|||
return ContextCompat.getDrawable(this, resource)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to dp.
|
||||
*/
|
||||
val Int.pxToDp: Int
|
||||
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
|
||||
|
||||
val Float.pxToDp: Float
|
||||
get() = (this / Resources.getSystem().displayMetrics.density)
|
||||
|
||||
/**
|
||||
* Converts to px.
|
||||
*/
|
||||
val Int.dpToPx: Int
|
||||
get() = this.toFloat().dpToPx.toInt()
|
||||
|
||||
val Int.spToPx: Int
|
||||
get() = this.toFloat().spToPx.toInt()
|
||||
|
||||
val Float.spToPx: Float
|
||||
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics)
|
||||
|
||||
val Float.dpToPx: Float
|
||||
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics)
|
||||
|
||||
/** Converts to px and takes into account LTR/RTL layout */
|
||||
fun Float.dpToPxEnd(resources: Resources): Float {
|
||||
return this * resources.displayMetrics.density * if (resources.isLTR) 1 else -1
|
||||
|
|
|
@ -1116,12 +1116,6 @@
|
|||
<string name="file_picker_error">No file picker app found</string>
|
||||
<string name="file_picker_uri_permission_unsupported">Failed to acquire persistent folder access. The app may behave unexpectedly.</string>
|
||||
|
||||
<!-- Webview -->
|
||||
<string name="failed_to_bypass_cloudflare">Failed to bypass Cloudflare</string>
|
||||
<string name="please_update_webview">Please update the WebView app for better compatibility</string>
|
||||
<!-- Do not translate "WebView" -->
|
||||
<string name="webview_is_required">WebView is required for Tachiyomi</string>
|
||||
|
||||
<!-- App widget -->
|
||||
<string name="appwidget_updates_description">See your recently updated library entries</string>
|
||||
<string name="appwidget_unavailable_locked">Widget not available when app lock is enabled</string>
|
||||
|
|
|
@ -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 {
|
||||
|
@ -15,6 +19,7 @@ buildscript {
|
|||
classpath(kotlinx.serialization.gradle)
|
||||
classpath(libs.firebase.crashlytics.gradle)
|
||||
classpath(libs.sqldelight.gradle)
|
||||
classpath(libs.moko.generator)
|
||||
}
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
|
@ -23,6 +28,45 @@ buildscript {
|
|||
}
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.withType<KotlinJvmCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
testLogging {
|
||||
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||
}
|
||||
}
|
||||
|
||||
plugins.withType<BasePlugin> {
|
||||
configure<BaseExtension> {
|
||||
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) }
|
||||
|
|
41
buildSrc/src/main/kotlin/LocalesConfigPlugin.kt
Normal file
41
buildSrc/src/main/kotlin/LocalesConfigPlugin.kt
Normal file
|
@ -0,0 +1,41 @@
|
|||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.tasks.TaskProvider
|
||||
import org.gradle.kotlin.dsl.TaskContainerScope
|
||||
|
||||
fun TaskContainerScope.registerLocalesConfigTask(project: Project): TaskProvider<Task> {
|
||||
return with(project) {
|
||||
register("generateLocalesConfig") {
|
||||
val emptyResourcesElement = "<resources>\\s*</resources>|<resources/>".toRegex()
|
||||
val valuesPrefix = "values-?".toRegex()
|
||||
|
||||
val languages = fileTree("$projectDir/src/main/res/")
|
||||
.matching {
|
||||
include("**/strings.xml")
|
||||
}
|
||||
.filterNot {
|
||||
it.readText().contains(emptyResourcesElement)
|
||||
}
|
||||
.map { it.parentFile.name }
|
||||
.sorted()
|
||||
.joinToString(separator = "\n") {
|
||||
val language = it
|
||||
.replace(valuesPrefix, "")
|
||||
.replace("-r", "-")
|
||||
.takeIf(String::isNotBlank) ?: "en"
|
||||
" <locale android:name=\"$language\"/>"
|
||||
}
|
||||
|
||||
val content = """
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
$languages
|
||||
</locale-config>
|
||||
""".trimIndent()
|
||||
|
||||
val localeFile = file("$projectDir/src/main/res/xml/locales_config.xml")
|
||||
localeFile.parentFile.mkdirs()
|
||||
localeFile.writeText(content)
|
||||
}
|
||||
}
|
||||
}
|
49
core/build.gradle.kts
Normal file
49
core/build.gradle.kts
Normal file
|
@ -0,0 +1,49 @@
|
|||
plugins {
|
||||
kotlin("multiplatform")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidTarget()
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
implementation(projects.i18n)
|
||||
api(libs.bundles.logging)
|
||||
}
|
||||
}
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
api(libs.okhttp)
|
||||
api(libs.okhttp.logging.interceptor)
|
||||
api(libs.okhttp.dnsoverhttps)
|
||||
api(libs.okhttp.brotli)
|
||||
api(libs.okio)
|
||||
|
||||
api(androidx.preference)
|
||||
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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xcontext-receivers",
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
)
|
||||
}
|
||||
}
|
2
core/src/androidMain/AndroidManifest.xml
Normal file
2
core/src/androidMain/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -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"
|
|
@ -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()
|
|
@ -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()
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.AndroidCookieJar
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
|
@ -15,8 +14,10 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getMString
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.*
|
||||
|
||||
class CloudflareInterceptor(
|
||||
private val context: Context,
|
||||
|
@ -48,7 +49,7 @@ class CloudflareInterceptor(
|
|||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||
// we don't crash the entire app
|
||||
catch (e: CloudflareBypassException) {
|
||||
throw IOException(context.getString(R.string.failed_to_bypass_cloudflare))
|
||||
throw IOException(context.getMString(MR.strings.failed_to_bypass_cloudflare))
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
}
|
||||
|
@ -130,7 +131,7 @@ class CloudflareInterceptor(
|
|||
if (!cloudflareBypassed) {
|
||||
// Prompt user to update WebView if it seems too outdated
|
||||
if (isWebViewOutdated) {
|
||||
context.toast(R.string.please_update_webview, Toast.LENGTH_LONG)
|
||||
context.toast(MR.strings.please_update_webview, Toast.LENGTH_LONG)
|
||||
}
|
||||
|
||||
throw CloudflareBypassException()
|
|
@ -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.
|
|
@ -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.
|
|
@ -5,7 +5,6 @@ import android.os.Build
|
|||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.launchUI
|
||||
|
@ -15,9 +14,9 @@ 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 yokai.i18n.MR
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
|
||||
abstract class WebViewInterceptor(
|
||||
private val context: Context,
|
||||
|
@ -57,7 +56,7 @@ abstract class WebViewInterceptor(
|
|||
|
||||
if (!WebViewUtil.supportsWebView(context)) {
|
||||
launchUI {
|
||||
context.toast(R.string.webview_is_required, Toast.LENGTH_LONG)
|
||||
context.toast(MR.strings.webview_is_required, Toast.LENGTH_LONG)
|
||||
}
|
||||
return response
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.util.TypedValue
|
||||
|
||||
/**
|
||||
* Converts to dp.
|
||||
*/
|
||||
val Int.pxToDp: Int
|
||||
get() = (this / Resources.getSystem().displayMetrics.density).toInt()
|
||||
|
||||
val Float.pxToDp: Float
|
||||
get() = (this / Resources.getSystem().displayMetrics.density)
|
||||
|
||||
/**
|
||||
* Converts to px.
|
||||
*/
|
||||
val Int.dpToPx: Int
|
||||
get() = this.toFloat().dpToPx.toInt()
|
||||
|
||||
val Int.spToPx: Int
|
||||
get() = this.toFloat().spToPx.toInt()
|
||||
|
||||
val Float.spToPx: Float
|
||||
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics)
|
||||
|
||||
val Float.dpToPx: Float
|
||||
get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics)
|
|
@ -0,0 +1,37 @@
|
|||
package eu.kanade.tachiyomi.util.system
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import yokai.util.lang.getMString
|
||||
|
||||
/**
|
||||
* 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 resource the text resource.
|
||||
* @param duration the duration of the toast. Defaults to short.
|
||||
*/
|
||||
fun Context.toast(resource: StringResource, duration: Int = Toast.LENGTH_SHORT) {
|
||||
toast(getMString(resource), duration)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package yokai.util.lang
|
||||
|
||||
import android.content.Context
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.desc.Resource
|
||||
import dev.icerock.moko.resources.desc.StringDesc
|
||||
|
||||
fun Context.getMString(stringRes: StringResource): String = StringDesc.Resource(stringRes).toString(this)
|
|
@ -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 <T> Observable<T>.awaitSingle(): T = single().awaitOne()
|
||||
|
||||
@OptIn(InternalCoroutinesApi::class)
|
||||
private suspend fun <T> Observable<T>.awaitOne(): T = suspendCancellableCoroutine { cont ->
|
||||
cont.unsubscribeOnCancellation(
|
||||
subscribe(
|
|
@ -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<Boolean>.toggle(): Boolean {
|
|||
set(!get())
|
||||
return get()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> Preference<T>.collectAsState(): State<T> {
|
||||
val flow = remember(this) { changes() }
|
||||
return flow.collectAsState(initial = get())
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class NetworkPreferences(private val preferenceStore: PreferenceStore) {
|
||||
|
||||
fun dohProvider() = preferenceStore.getInt("doh_provider", -1)
|
||||
|
||||
fun defaultUserAgent() = preferenceStore.getString("default_user_agent", NetworkHelper.DEFAULT_USER_AGENT)
|
||||
}
|
11
domain/build.gradle.kts
Normal file
11
domain/build.gradle.kts
Normal file
|
@ -0,0 +1,11 @@
|
|||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "yokai.domain"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
|
@ -20,7 +20,7 @@
|
|||
# AndroidX support
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
org.gradle.jvmargs=-Xmx4G
|
||||
org.gradle.caching=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -3,6 +3,7 @@ chucker = "3.5.2"
|
|||
coil3 = "3.0.0-alpha06"
|
||||
flexible-adapter = "c8013533"
|
||||
fast_adapter = "5.6.0"
|
||||
moko = "0.24.0"
|
||||
nucleus = "3.0.0"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
shizuku = "12.1.0"
|
||||
|
@ -59,6 +60,10 @@ junit-android = { module = "androidx.test.ext:junit", version = "1.1.5" }
|
|||
junrar = { module = "com.github.junrar:junrar", version = "7.5.5" }
|
||||
loading-button = { module = "br.com.simplepass:loading-button-android", version = "2.2.0" }
|
||||
mockk = { module = "io.mockk:mockk", version = "1.13.11" }
|
||||
|
||||
moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko" }
|
||||
moko-generator = { module = "dev.icerock.moko:resources-generator", version.ref = "moko" }
|
||||
|
||||
okio = { module = "com.squareup.okio:okio", version = "3.9.0" }
|
||||
okhttp-brotli = { module = "com.squareup.okhttp3:okhttp-brotli", version.ref = "okhttp" }
|
||||
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp" }
|
||||
|
|
2
i18n/.gitignore
vendored
Normal file
2
i18n/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Generated
|
||||
locales_config.xml
|
43
i18n/build.gradle.kts
Normal file
43
i18n/build.gradle.kts
Normal file
|
@ -0,0 +1,43 @@
|
|||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("com.android.library")
|
||||
id("dev.icerock.mobile.multiplatform-resources")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidTarget()
|
||||
|
||||
applyDefaultHierarchyTemplate()
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
api(libs.moko.resources)
|
||||
}
|
||||
}
|
||||
val androidMain by getting {
|
||||
dependsOn(commonMain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "yokai.i18n"
|
||||
}
|
||||
|
||||
multiplatformResources {
|
||||
resourcesPackage.set("yokai.i18n")
|
||||
}
|
||||
|
||||
tasks {
|
||||
val localesConfigTask = registerLocalesConfigTask(project)
|
||||
preBuild {
|
||||
dependsOn(localesConfigTask)
|
||||
}
|
||||
|
||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xexpect-actual-classes",
|
||||
)
|
||||
}
|
||||
}
|
2
i18n/src/androidMain/AndroidManifest.xml
Normal file
2
i18n/src/androidMain/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
8
i18n/src/commonMain/moko-resources/base/strings.xml
Normal file
8
i18n/src/commonMain/moko-resources/base/strings.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- WebView -->
|
||||
<string name="failed_to_bypass_cloudflare">Failed to bypass Cloudflare</string>
|
||||
<!-- Do not translate "WebView" -->
|
||||
<string name="please_update_webview">Please update the WebView app for better compatibility</string>
|
||||
<string name="webview_is_required">WebView is required for Tachiyomi</string>
|
||||
</resources>
|
17
presentation-core/build.gradle.kts
Normal file
17
presentation-core/build.gradle.kts
Normal file
|
@ -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 {
|
||||
|
||||
}
|
0
presentation-core/consumer-rules.pro
Normal file
0
presentation-core/consumer-rules.pro
Normal file
21
presentation-core/proguard-rules.pro
vendored
Normal file
21
presentation-core/proguard-rules.pro
vendored
Normal file
|
@ -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
|
31
presentation-widget/build.gradle.kts
Normal file
31
presentation-widget/build.gradle.kts
Normal file
|
@ -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(projects.core)
|
||||
implementation(projects.domain)
|
||||
implementation(projects.presentationCore)
|
||||
|
||||
implementation(androidx.glance.appwidget)
|
||||
|
||||
implementation(libs.coil3)
|
||||
}
|
0
presentation-widget/consumer-rules.pro
Normal file
0
presentation-widget/consumer-rules.pro
Normal file
21
presentation-widget/proguard-rules.pro
vendored
Normal file
21
presentation-widget/proguard-rules.pro
vendored
Normal file
|
@ -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
|
|
@ -0,0 +1,15 @@
|
|||
package eu.kanade.tachiyomi.appwidget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import eu.kanade.tachiyomi.appwidget.UpdatesGridGlanceWidget
|
||||
|
||||
class TachiyomiWidgetManager {
|
||||
|
||||
suspend fun Context.init() {
|
||||
val manager = GlanceAppWidgetManager(this)
|
||||
if (manager.getGlanceIds(UpdatesGridGlanceWidget::class.java).isNotEmpty()) {
|
||||
UpdatesGridGlanceWidget().loadData()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package eu.kanade.tachiyomi.appwidget
|
||||
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.GlanceAppWidgetReceiver
|
||||
|
||||
class UpdatesGridGlanceReceiver : GlanceAppWidgetReceiver() {
|
||||
override val glanceAppWidget: GlanceAppWidget = UpdatesGridGlanceWidget().apply { loadData() }
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package eu.kanade.tachiyomi.appwidget
|
||||
|
||||
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<Pair<Long, Bitmap?>>? = 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<Pair<Manga, Long>>? = 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<Pair<Manga, Long>>, take: Int): List<Pair<Long, Bitmap?>> {
|
||||
// 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()
|
|
@ -0,0 +1,44 @@
|
|||
package eu.kanade.tachiyomi.appwidget.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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package eu.kanade.tachiyomi.appwidget.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package eu.kanade.tachiyomi.appwidget.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.appwidget.ContainerModifier
|
||||
import eu.kanade.tachiyomi.appwidget.util.calculateRowAndColumnCount
|
||||
import eu.kanade.tachiyomi.appwidget.util.stringResource
|
||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||
|
||||
@Composable
|
||||
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
|
||||
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
|
||||
val clazz = Class.forName("eu.kanade.tachiyomi.ui.main.MainActivity")
|
||||
val mainIntent = Intent(LocalContext.current, clazz).setAction("eu.kanade.tachiyomi.SHOW_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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package eu.kanade.tachiyomi.appwidget.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<Int, Int> {
|
||||
// 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)
|
||||
}
|
|
@ -27,5 +27,13 @@ dependencyResolutionManagement {
|
|||
}
|
||||
}
|
||||
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
rootProject.name = "Yokai"
|
||||
include(":app")
|
||||
include(":core")
|
||||
include(":domain")
|
||||
include(":i18n")
|
||||
include(":presentation-core")
|
||||
include(":presentation-widget")
|
||||
include(":source-api")
|
||||
|
|
42
source-api/build.gradle.kts
Normal file
42
source-api/build.gradle.kts
Normal file
|
@ -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<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
kotlinOptions.freeCompilerArgs += listOf(
|
||||
"-Xexpect-actual-classes",
|
||||
)
|
||||
}
|
||||
}
|
5
source-api/consumer-proguard.pro
Normal file
5
source-api/consumer-proguard.pro
Normal file
|
@ -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 *; }
|
2
source-api/src/androidMain/AndroidManifest.xml
Normal file
2
source-api/src/androidMain/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
|
@ -0,0 +1,6 @@
|
|||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import rx.Observable
|
||||
import yokai.util.lang.awaitSingle
|
||||
|
||||
actual suspend fun <T> Observable<T>.awaitSingle(): T = awaitSingle()
|
|
@ -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 {
|
|
@ -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<String>, extensionManager: ExtensionManager? = null): Boolean {
|
||||
val httpSource = this as? HttpSource ?: return true
|
||||
val extManager = extensionManager ?: Injekt.get()
|
||||
val allExt = httpSource.getExtension(extManager)?.lang == "all"
|
||||
val onlyAll = httpSource.extOnlyHasAllLanguage(extManager)
|
||||
val isMultiLingual = enabledLanguages.filterNot { it == "all" }.size > 1
|
||||
return (isMultiLingual && allExt) || (lang == "all" && !onlyAll)
|
||||
}
|
||||
|
||||
fun nameBasedOnEnabledLanguages(enabledLanguages: Set<String>, extensionManager: ExtensionManager? = null): String {
|
||||
return if (includeLangInName(enabledLanguages, extensionManager)) toString() else name
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getMangaDetails"),
|
||||
|
@ -101,6 +83,4 @@ interface Source {
|
|||
throw IllegalStateException("Not used")
|
||||
}
|
||||
|
||||
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
|
||||
|
||||
fun Source.preferenceKey(): String = "source_$id"
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue