mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
* Add Apollo for GraphQL interactions * Migrate AniList to use Apollo * Migrate Shikimori to use Apollo Not all Shikimori requests can be served by their GraphQL API (notably they do not offer any mutations), so those remain as-is. * Migrate Suwayomi to use Apollo Includes leveraging some slightly newer Suwayomi features, specifically the Double/Float filter fixes first pre-released in 2.1.1985 on 2025-10-26 and first fully released in 2.1.2100 on 2026-05-08. Considering the tachiyomix 1.6 extension lib changes were added in Suwayomi 2.3.2223 on 2026-06-30, I think it is fair to depend on such sorta-recent behaviour. * Fix temp migration names and leftovers * Create ApolloResponse.dataOrElse extension Because Apollo doesn't throw exceptions and instead just provides them as response attributes, the current ApolloResponse.awaitSuccess() is useless and will be removed shortly. This new extension function should wrap the basic handling of an ApolloResponse object neatly. Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com> * Use dataOrElse in AnilistApi.kt * Use dataOrElse in ShikimoriApi.kt * Use dataOrElse in SuwayomiApi.kt * Delete obsolete ApolloCall.awaitSuccess * Migrate Kitsu to use Apollo Kitsu has some unfortunate edges in its GraphQL API (such as returning nonstandard bodies which don't play well with Apollo) but this should be an equivalent implementation. * Extract a MangaFragment for AniList This simplifies the queries and the converters (which can now delegate to the MangaFragment.toTrackSearch() method internally and adjust the track as needed). Also move the toALUser converter to AnilistUtils.kt to keep all converters together. Added a scalar-mapper for AL's CountryCode type. * Minor consistency cleanup of the queries * Store list entry ID not title ID for Shikimori * Extract common MangaFragment for Shikimori Also map the ISO8601Date to a Kotlin String in the service config. * Map Kitsu's ISO8601DateTime to kotlin.time.Instant Utilises Apollo's own adapter-core library which provides the mapper. Requires opting in to ExperimentalTime until the library resolves the outdated annotation. Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com> * Replace Kitsu TODO comment So much for STOPSHIP preventing me shipping this... * Bump Apollo version to 5.1.0 * Bump apollo-adapters-core to 0.8.0 Freshly released just earlier. This resolves having to opt in to ExperimentalTime. * Make new utils internal to align with #3908 * Fix leftover issues from merged #3828 This PR restructured the AniList and Kitsu user objects. * Bump apollo to 5.2.0 * Remove unused attributes from queries * Allow custom exception handler in dataOrElse Enables callers to manually handle specific non-200 responses. Callers MUST close the `ApolloHttpException.body` attached to the passed exception to avoid resource and socket leaks. Defaults to current behaviour of (re)throwing the status code with our HttpException. Utilized in KitsuApi.kt to specially handle Kitsu sending a 500 error response when deleting library IDs not actually in a user's library and falling back to the "throw as HttpException" if it's any other error response. * Rethrow networking/IO exceptions This helps to avoid masking networking problems as "This isn't in your list" problems. Currently, the app shows "Failed to connect to ..." when the remote server is unreachable (e.g. downtime, DNS block, etc) because that's the message provided by the ConnectException thrown by Android's network stack. This change restores that behaviour, approximately. Not the most elegant solution but it _should_ avoid false-positive toasts. --------- Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com>
452 lines
14 KiB
Kotlin
452 lines
14 KiB
Kotlin
import com.android.build.api.variant.ApplicationVariant
|
|
import com.android.build.api.variant.BuildConfigField
|
|
import com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension
|
|
import mihon.gradle.Config
|
|
import mihon.gradle.getCurrentTime
|
|
import mihon.gradle.getLatestCommitCount
|
|
import mihon.gradle.getLatestCommitSha
|
|
import mihon.gradle.getLatestCommitTime
|
|
import mihon.gradle.tasks.ReplaceShortcutsPlaceholderTask
|
|
import java.io.FileInputStream
|
|
import java.util.Properties
|
|
|
|
plugins {
|
|
alias(mihonx.plugins.android.application)
|
|
alias(mihonx.plugins.compose)
|
|
alias(mihonx.plugins.spotless)
|
|
|
|
alias(libs.plugins.metro)
|
|
alias(libs.plugins.aboutLibraries)
|
|
alias(libs.plugins.androidx.baselineProfile)
|
|
alias(libs.plugins.kotlin.serialization)
|
|
alias(libs.plugins.apollo)
|
|
}
|
|
|
|
if (Config.includeTelemetry) {
|
|
pluginManager.apply {
|
|
apply(libs.plugins.google.services.get().pluginId)
|
|
apply(libs.plugins.firebase.crashlytics.get().pluginId)
|
|
}
|
|
}
|
|
|
|
val keystorePropertiesFile = layout.settingsDirectory.file("keystore.properties").asFile
|
|
|
|
android {
|
|
namespace = "eu.kanade.tachiyomi"
|
|
|
|
defaultConfig {
|
|
applicationId = "app.mihon"
|
|
|
|
versionCode = 32
|
|
versionName = "0.20.4"
|
|
|
|
buildConfigField("boolean", "TELEMETRY_INCLUDED", "${Config.includeTelemetry}")
|
|
buildConfigField("boolean", "UPDATER_ENABLED", "${Config.enableUpdater}")
|
|
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
}
|
|
|
|
if (System.getenv("MIHON_GITHUB_RELEASE").toBoolean()) {
|
|
signingConfigs {
|
|
named("debug") {
|
|
storeFile = file(System.getenv("storeFile"))
|
|
storePassword = System.getenv("storePassword")
|
|
keyAlias = System.getenv("keyAlias")
|
|
keyPassword = System.getenv("keyPassword")
|
|
}
|
|
}
|
|
} else if (keystorePropertiesFile.exists()) {
|
|
val keystoreProperties = FileInputStream(keystorePropertiesFile).use { Properties().apply { load(it) } }
|
|
|
|
signingConfigs {
|
|
named("debug") {
|
|
storeFile = file(keystoreProperties.getProperty("storeFile"))
|
|
storePassword = keystoreProperties.getProperty("storePassword")
|
|
keyAlias = keystoreProperties.getProperty("keyAlias")
|
|
keyPassword = keystoreProperties.getProperty("keyPassword")
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
val debug = getByName("debug") {
|
|
applicationIdSuffix = ".dev"
|
|
isPseudoLocalesEnabled = true
|
|
}
|
|
val release = getByName("release") {
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
|
|
signingConfig = debug.signingConfig
|
|
|
|
isProfileable = true
|
|
|
|
proguardFiles("proguard-android-optimize.txt", "proguard-rules.pro")
|
|
}
|
|
|
|
val commonMatchingFallbacks = listOf(release.name)
|
|
|
|
create("foss") {
|
|
initWith(release)
|
|
|
|
applicationIdSuffix = ".foss"
|
|
|
|
matchingFallbacks.addAll(commonMatchingFallbacks)
|
|
}
|
|
create("nightly") {
|
|
initWith(release)
|
|
|
|
applicationIdSuffix = ".debug"
|
|
|
|
matchingFallbacks.addAll(commonMatchingFallbacks)
|
|
}
|
|
create("benchmark") {
|
|
initWith(release)
|
|
|
|
versionNameSuffix = "-benchmark"
|
|
applicationIdSuffix = ".benchmark"
|
|
|
|
matchingFallbacks.addAll(commonMatchingFallbacks)
|
|
}
|
|
|
|
if (Config.includeTelemetry) {
|
|
configureEach {
|
|
configure<CrashlyticsExtension> {
|
|
mappingFileUploadEnabled = Config.uploadCrashlyticsMapping
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sourceSets {
|
|
getByName("nightly").res.directories.add("src/debug/res")
|
|
getByName("benchmark").res.directories.add("src/debug/res")
|
|
}
|
|
|
|
splits {
|
|
abi {
|
|
isEnable = true
|
|
isUniversalApk = true
|
|
reset()
|
|
include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
|
}
|
|
}
|
|
|
|
packaging {
|
|
jniLibs {
|
|
keepDebugSymbols += listOf(
|
|
"libandroidx.graphics.path",
|
|
"libarchive-jni",
|
|
"libconscrypt_jni",
|
|
"libimagedecoder",
|
|
"libquickjs",
|
|
"libsqlite3x",
|
|
)
|
|
.map { "**/$it.so" }
|
|
}
|
|
resources {
|
|
excludes += setOf(
|
|
"kotlin-tooling-metadata.json",
|
|
"LICENSE.txt",
|
|
"META-INF/**/*.properties",
|
|
"META-INF/**/LICENSE.txt",
|
|
"META-INF/*.properties",
|
|
"META-INF/*.version",
|
|
"META-INF/DEPENDENCIES",
|
|
"META-INF/LICENSE",
|
|
"META-INF/NOTICE",
|
|
"META-INF/README.md",
|
|
)
|
|
}
|
|
}
|
|
|
|
dependenciesInfo {
|
|
includeInApk = Config.includeDependencyInfo
|
|
includeInBundle = Config.includeDependencyInfo
|
|
}
|
|
|
|
buildFeatures {
|
|
viewBinding = true
|
|
buildConfig = true
|
|
aidl = true
|
|
}
|
|
|
|
lint {
|
|
abortOnError = false
|
|
checkReleaseBuilds = false
|
|
}
|
|
}
|
|
|
|
kotlin {
|
|
compilerOptions {
|
|
freeCompilerArgs.addAll(
|
|
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
|
|
"-opt-in=androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi",
|
|
"-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
|
"-opt-in=androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
|
|
"-opt-in=androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
|
"-opt-in=androidx.compose.ui.ExperimentalComposeUiApi",
|
|
"-opt-in=coil3.annotation.ExperimentalCoilApi",
|
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
|
"-opt-in=kotlinx.coroutines.FlowPreview",
|
|
"-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
|
|
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
|
)
|
|
}
|
|
}
|
|
|
|
baselineProfile {
|
|
baselineProfileOutputDir = "baselineProfiles"
|
|
mergeIntoMain = true
|
|
}
|
|
|
|
dependencies {
|
|
baselineProfile(projects.baselineProfile)
|
|
|
|
implementation(projects.i18n)
|
|
implementation(projects.icons.materialSymbols)
|
|
implementation(projects.icons.simpleIcons)
|
|
implementation(projects.core.archive)
|
|
implementation(projects.core.common)
|
|
implementation(projects.core.metro)
|
|
implementation(projects.coreMetadata)
|
|
implementation(projects.sourceApi)
|
|
implementation(projects.sourceLocal)
|
|
implementation(projects.data)
|
|
implementation(projects.domain)
|
|
implementation(projects.presentationCore)
|
|
implementation(projects.presentationWidget)
|
|
implementation(projects.telemetry)
|
|
|
|
// Compose
|
|
implementation(libs.androidx.activity.compose)
|
|
implementation(libs.androidx.compose.foundation)
|
|
implementation(libs.androidx.compose.material3)
|
|
implementation(libs.androidx.compose.animation)
|
|
implementation(libs.androidx.compose.animationGraphics)
|
|
debugImplementation(libs.androidx.compose.uiTooling)
|
|
implementation(libs.androidx.compose.uiToolingPreview)
|
|
implementation(libs.androidx.compose.uiUtil)
|
|
|
|
implementation(libs.androidx.interpolator)
|
|
|
|
implementation(libs.androidx.paging.runtime)
|
|
implementation(libs.androidx.paging.compose)
|
|
|
|
implementation(libs.kotlin.reflect)
|
|
|
|
implementation(libs.bundles.kotlinx.coroutines)
|
|
|
|
implementation(libs.kotlinx.datetime)
|
|
|
|
// AndroidX libraries
|
|
implementation(libs.androidx.annotation)
|
|
implementation(libs.androidx.appCompat)
|
|
implementation(libs.androidx.biometric)
|
|
implementation(libs.androidx.constraintLayout)
|
|
implementation(libs.androidx.core)
|
|
implementation(libs.androidx.coreSplashScreen)
|
|
implementation(libs.androidx.recyclerView)
|
|
implementation(libs.androidx.viewPager)
|
|
implementation(libs.androidx.profileInstaller)
|
|
|
|
implementation(libs.bundles.androidx.lifecycle)
|
|
|
|
// Job scheduling
|
|
implementation(libs.androidx.work)
|
|
|
|
// RxJava
|
|
implementation(libs.rxJava)
|
|
|
|
// Networking
|
|
implementation(libs.bundles.okhttp)
|
|
implementation(libs.okio)
|
|
implementation(libs.conscrypt) // TLS 1.3 support for Android < 10
|
|
|
|
// Data serialization (JSON, protobuf, xml)
|
|
implementation(libs.bundles.serialization)
|
|
|
|
// HTML parser
|
|
implementation(libs.jsoup)
|
|
|
|
// Disk
|
|
implementation(libs.diskLruCache)
|
|
implementation(libs.unifile)
|
|
|
|
// Preferences
|
|
implementation(libs.androidx.preference)
|
|
|
|
// Dependency injection
|
|
implementation(libs.injekt)
|
|
implementation(libs.metro.runtime)
|
|
implementation(libs.metrox.viewmodel)
|
|
implementation(libs.metrox.viewmodel.compose)
|
|
|
|
// Image loading
|
|
implementation(libs.bundles.coil)
|
|
implementation(libs.subsamplingScaleImageView) {
|
|
exclude(module = "image-decoder")
|
|
}
|
|
implementation(libs.image.decoder)
|
|
|
|
implementation(libs.webgpuviewer)
|
|
|
|
// UI libraries
|
|
implementation(libs.material)
|
|
implementation(libs.flexibleAdapter)
|
|
implementation(libs.photoView)
|
|
implementation(libs.directionalViewPager) {
|
|
exclude(group = "androidx.viewpager", module = "viewpager")
|
|
}
|
|
implementation(libs.composeRichEditor)
|
|
implementation(libs.aboutLibraries.compose)
|
|
implementation(libs.bundles.voyager)
|
|
implementation(libs.composeMaterialMotion)
|
|
implementation(libs.swipe)
|
|
implementation(libs.composeWebview)
|
|
implementation(libs.composeGrid)
|
|
implementation(libs.reorderable)
|
|
implementation(libs.bundles.markdown)
|
|
implementation(libs.materialKolor)
|
|
|
|
// Logging
|
|
implementation(libs.logcat)
|
|
|
|
// Shizuku
|
|
implementation(libs.bundles.shizuku)
|
|
|
|
// String similarity
|
|
implementation(libs.stringSimilarity)
|
|
|
|
// GraphQL generation
|
|
implementation(libs.apollo)
|
|
implementation(libs.apollo.adapters)
|
|
|
|
// Tests
|
|
testImplementation(libs.bundles.test)
|
|
testRuntimeOnly(libs.junit.platform.launcher)
|
|
|
|
// For detecting memory leaks; see https://square.github.io/leakcanary/
|
|
// debugImplementation(libs.leakCanary.android)
|
|
implementation(libs.leakCanary.plumber)
|
|
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
}
|
|
|
|
apollo {
|
|
val schemaBasePath = "src/main/graphql/mihon/graphql"
|
|
service("anilist") {
|
|
packageName.set("mihon.graphql.anilist")
|
|
val srcDir = "$schemaBasePath/anilist"
|
|
srcDir(file(srcDir))
|
|
|
|
introspection {
|
|
endpointUrl.set("https://graphql.anilist.co")
|
|
schemaFile.set(file("$srcDir/anilist.graphqls"))
|
|
}
|
|
|
|
// ISO 3166-1 alpha-2 country code
|
|
mapScalarToKotlinString("CountryCode")
|
|
}
|
|
|
|
service("kitsu") {
|
|
packageName.set("mihon.graphql.kitsu")
|
|
val srcDir = "$schemaBasePath/kitsu"
|
|
srcDir(file(srcDir))
|
|
android.defaultConfig.versionCode
|
|
|
|
introspection {
|
|
// Kitsu doesn't like requests without a UA
|
|
headers.put(
|
|
"User-Agent",
|
|
"Mihon v${android.defaultConfig.versionName} (${android.defaultConfig.applicationId})",
|
|
)
|
|
endpointUrl.set("https://kitsu.app/api/graphql")
|
|
schemaFile.set(file("$srcDir/kitsu.graphqls"))
|
|
}
|
|
|
|
// A date, expressed as an ISO8601 string
|
|
mapScalarToKotlinString("Date")
|
|
// An ISO 8601-encoded datetime
|
|
mapScalar("ISO8601DateTime", "kotlin.time.Instant", "com.apollographql.adapter.core.KotlinInstantAdapter")
|
|
// A loose key-value map in GraphQL
|
|
mapScalar(
|
|
"Map",
|
|
"kotlin.collections.Map<String, Any?>",
|
|
"mihon.graphql.kitsu.adapter.KitsuMapAdapter",
|
|
)
|
|
}
|
|
|
|
service("shikimori") {
|
|
packageName.set("mihon.graphql.shikimori")
|
|
val srcDir = "$schemaBasePath/shikimori"
|
|
srcDir(file(srcDir))
|
|
|
|
introspection {
|
|
endpointUrl.set("https://shikimori.io/api/graphql")
|
|
schemaFile.set(file("$srcDir/shikimori.graphqls"))
|
|
}
|
|
|
|
// An ISO 8601-encoded date
|
|
mapScalarToKotlinString("ISO8601Date")
|
|
}
|
|
|
|
service("suwayomi") {
|
|
packageName.set("mihon.graphql.suwayomi")
|
|
val srcDir = "$schemaBasePath/suwayomi"
|
|
srcDir(file(srcDir))
|
|
|
|
introspection {
|
|
endpointUrl.set("http://localhost:4567/api/graphql")
|
|
schemaFile.set(file("$srcDir/suwayomi.graphqls"))
|
|
}
|
|
}
|
|
}
|
|
|
|
val latestCommitCount = getLatestCommitCount()
|
|
val latestCommitSha = getLatestCommitSha()
|
|
val latestCommitTime = getLatestCommitTime()
|
|
val currentTime = getCurrentTime()
|
|
|
|
fun ApplicationVariant.buildConfigField(type: String, name: String, value: Provider<String>) {
|
|
buildConfigFields?.put(name, value.map { BuildConfigField(type, it, null) })
|
|
}
|
|
|
|
androidComponents {
|
|
onVariants { variant ->
|
|
val isUnstableBuild = variant.buildType == "debug" || variant.buildType == "nightly"
|
|
val buildTime = if (isUnstableBuild) currentTime else latestCommitTime
|
|
|
|
variant.buildConfigField("String", "COMMIT_COUNT", latestCommitCount.map { "\"$it\"" })
|
|
variant.buildConfigField("String", "COMMIT_SHA", latestCommitSha.map { "\"$it\"" })
|
|
variant.buildConfigField("String", "BUILD_TIME", buildTime.map { "\"$it\"" })
|
|
|
|
if (isUnstableBuild) {
|
|
variant.outputs.forEach { output ->
|
|
val versionName = output.versionName.get()
|
|
output.versionName.set(latestCommitCount.map { "$versionName-$it" })
|
|
}
|
|
}
|
|
}
|
|
|
|
onVariants { variant ->
|
|
val resSource = variant.sources.res ?: return@onVariants
|
|
|
|
val variantName = variant.name.replaceFirstChar { it.uppercase() }
|
|
val replaceShortcutsPlaceholderTask = tasks.register<ReplaceShortcutsPlaceholderTask>(
|
|
"replace${variantName}ShortcutPlaceholder",
|
|
) {
|
|
applicationId.set(variant.applicationId)
|
|
shortcutsFile.set(projectDir.resolve("src/main/shortcuts.xml"))
|
|
}
|
|
resSource.addGeneratedSourceDirectory(replaceShortcutsPlaceholderTask) { it.outputDir }
|
|
}
|
|
|
|
onVariants(selector().withFlavor("default" to "standard")) {
|
|
// Only excluding in standard flavor because this breaks
|
|
// Layout Inspector's Compose tree
|
|
it.packaging.resources.excludes.add("META-INF/*.version")
|
|
}
|
|
}
|