mirror of
https://github.com/null2264/yokai.git
synced 2025-07-17 14:26:54 +00:00
refactor: Modularize the project
This commit is contained in:
parent
7e7a37bc53
commit
2df2780912
85 changed files with 1358 additions and 248 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,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<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(
|
||||
|
|
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
|
@ -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 *; }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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<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) }
|
||||
|
|
48
core/build.gradle.kts
Normal file
48
core/build.gradle.kts
Normal file
|
@ -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<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 />
|
|
@ -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<T>(
|
|||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.HONEYCOMB)
|
||||
class StringSetPrimitive(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
|
@ -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"
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
|
@ -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.
|
|
@ -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,
|
|
@ -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()
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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()
|
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 {
|
||||
}
|
|
@ -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<String>? {
|
||||
return genre?.split(",")
|
||||
?.mapNotNull { tag -> tag.trim().takeUnless { it.isBlank() } }
|
||||
}
|
||||
|
||||
fun getOriginalGenres(): List<String>? {
|
||||
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<SourceManager>().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<Int, Int>?
|
||||
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<Long, Int?> = 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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" }
|
||||
|
|
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(project(":core"))
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":presentation-core"))
|
||||
|
||||
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
|
14
presentation-widget/src/main/java/TachiyomiWidgetManager.kt
Normal file
14
presentation-widget/src/main/java/TachiyomiWidgetManager.kt
Normal file
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
128
presentation-widget/src/main/java/UpdatesGridGlanceWidget.kt
Normal file
128
presentation-widget/src/main/java/UpdatesGridGlanceWidget.kt
Normal file
|
@ -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<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()
|
44
presentation-widget/src/main/java/components/LockedWidget.kt
Normal file
44
presentation-widget/src/main/java/components/LockedWidget.kt
Normal file
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Pair<Long, Bitmap?>>?) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
43
presentation-widget/src/main/java/util/GlanceUtils.kt
Normal file
43
presentation-widget/src/main/java/util/GlanceUtils.kt
Normal file
|
@ -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<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,12 @@ dependencyResolutionManagement {
|
|||
}
|
||||
}
|
||||
|
||||
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||
|
||||
rootProject.name = "Yokai"
|
||||
include(":app")
|
||||
include(":core")
|
||||
include(":domain")
|
||||
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"
|
|
@ -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()
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
|
@ -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())
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import rx.Observable
|
||||
|
||||
expect suspend fun <T> Observable<T>.awaitSingle(): T
|
Loading…
Add table
Add a link
Reference in a new issue