package generator import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import java.io.File import java.nio.file.Files import java.nio.file.StandardCopyOption import java.util.Locale /** * This is meant to be used in place of a factory extension, specifically for what would be a multi-source extension. * A multi-lang (but not multi-source) extension should still be made as a factory extension. * Use a generator for initial setup of a theme source or when all of the inheritors need a version bump. * Source list (val sources) should be kept up to date. */ interface ThemeSourceGenerator { /** * The class that the sources inherit from. */ val themeClass: String /** * The package that contains themeClass. */ val themePkg: String /** * Base theme version, starts with 1 and should be increased when based theme class changes */ val baseVersionCode: Int /** * The list of sources to be created or updated. */ val sources: List fun createAll() { val userDir = System.getProperty("user.dir")!! sources.forEach { createGradleProject(it, themePkg, themeClass, baseVersionCode, userDir) } } companion object { private fun pkgNameSuffix(source: ThemeSourceData, separator: String): String { return if (source is ThemeSourceData.SingleLang) listOf(source.lang.substringBefore("-"), source.pkgName).joinToString(separator) else listOf("all", source.pkgName).joinToString(separator) } private fun themeSuffix(themePkg: String, separator: String): String { return listOf("eu", "kanade", "tachiyomi", "multisrc", themePkg).joinToString(separator) } private fun writeGradle(gradle: File, source: ThemeSourceData, themePkg: String, baseVersionCode: Int, defaultAdditionalGradlePath: String, additionalGradleOverridePath: String) { fun File.readTextOrEmptyString(): String = if (exists()) readText(Charsets.UTF_8) else "" val defaultAdditionalGradleText = File(defaultAdditionalGradlePath).readTextOrEmptyString() val additionalGradleOverrideText = File(additionalGradleOverridePath).readTextOrEmptyString() val placeholders = mapOf( "SOURCEHOST" to source.baseUrl.toHttpUrlOrNull()?.host, "SOURCESCHEME" to source.baseUrl.toHttpUrlOrNull()?.scheme ).filter { it.value != null } gradle.writeText( """ // THIS FILE IS AUTO-GENERATED; DO NOT EDIT apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlinx-serialization' ext { extName = '${source.name}' pkgNameSuffix = '${pkgNameSuffix(source, ".")}' extClass = '.${source.className}' extFactory = '$themePkg' extVersionCode = ${baseVersionCode + source.overrideVersionCode + multisrcLibraryVersion} ${if (source.isNsfw) "isNsfw = true\n" else ""} } $defaultAdditionalGradleText $additionalGradleOverrideText apply from: "${'$'}rootDir/common.gradle" android { defaultConfig { manifestPlaceholders += [ ${placeholders.map { "${" ".repeat(28)}${it.key}: \"${it.value}\""}.joinToString(",\n")} ] } } """.trimIndent() ) } private fun writeAndroidManifest(androidManifestFile: File, manifestOverridesPath: String, defaultAndroidManifestPath: String) { val androidManifestOverride = File(manifestOverridesPath) val defaultAndroidManifest = File(defaultAndroidManifestPath) if (androidManifestOverride.exists()) androidManifestOverride.copyTo(androidManifestFile) else if (defaultAndroidManifest.exists()) defaultAndroidManifest.copyTo(androidManifestFile) else androidManifestFile.writeText( """ """.trimIndent() ) } private fun createGradleProject(source: ThemeSourceData, themePkg: String, themeClass: String, baseVersionCode: Int, userDir: String) { val projectRootPath = "$userDir/generated-src/${pkgNameSuffix(source, "/")}" val projectSrcPath = "$projectRootPath/src/eu/kanade/tachiyomi/extension/${pkgNameSuffix(source, "/")}" val overridesPath = "$userDir/multisrc/overrides/$themePkg/${source.pkgName}" // userDir = tachiyomi-extensions project root path val defaultResPath = "$userDir/multisrc/overrides/$themePkg/default/res" val defaultAndroidManifestPath = "$userDir/multisrc/overrides/$themePkg/default/AndroidManifest.xml" val defaultAdditionalGradlePath = "$userDir/multisrc/overrides/$themePkg/default/additional.gradle.kts" val resOverridePath = "$overridesPath/res" val srcOverridePath = "$overridesPath/src" val manifestOverridePath = "$overridesPath/AndroidManifest.xml" val additionalGradleOverridePath = "$overridesPath/additional.gradle.kts" val projectGradleFile = File("$projectRootPath/build.gradle") val projectAndroidManifestFile = File("$projectRootPath/AndroidManifest.xml") File(projectRootPath).let { projectRootFile -> println("Working on $source") projectRootFile.mkdirs() // remove everything from past runs cleanDirectory(projectRootFile) writeGradle(projectGradleFile, source, themePkg, baseVersionCode, defaultAdditionalGradlePath, additionalGradleOverridePath) writeAndroidManifest(projectAndroidManifestFile, manifestOverridePath, defaultAndroidManifestPath) writeSourceClasses(projectSrcPath, srcOverridePath, source, themePkg, themeClass) copyThemeClasses(userDir, themePkg, projectRootPath) copyResFiles(resOverridePath, defaultResPath, source, projectRootPath) } } private fun copyThemeClasses(userDir: String, themePkg: String, projectRootPath: String) { val themeSrcPath = "$userDir/multisrc/src/main/java/${themeSuffix(themePkg, "/")}" val themeSrcFile = File(themeSrcPath) val themeDestPath = "$projectRootPath/src/${themeSuffix(themePkg, "/")}" val themeDestFile = File(themeDestPath) themeDestFile.mkdirs() themeSrcFile.list()!! .filter { it.endsWith(".kt") && !it.endsWith("Generator.kt") } .forEach { Files.copy(File("$themeSrcPath/$it").toPath(), File("$themeDestPath/$it").toPath(), StandardCopyOption.REPLACE_EXISTING) } } private fun copyResFiles(resOverridePath: String, defaultResPath: String, source: ThemeSourceData, projectRootPath: String): Any { // check if res override exists if not copy default res val resOverride = File(resOverridePath) return if (resOverride.exists()) resOverride.copyRecursively(File("$projectRootPath/res")) else File(defaultResPath).let { defaultResFile -> if (defaultResFile.exists()) defaultResFile.copyRecursively(File("$projectRootPath/res")) } } private fun writeSourceClasses(projectSrcPath: String, srcOverridePath: String, source: ThemeSourceData, themePkg: String, themeClass: String) { val projectSrcFile = File(projectSrcPath) projectSrcFile.mkdirs() val srcOverrideFile = File(srcOverridePath) if (srcOverrideFile.exists()) srcOverrideFile.copyRecursively(projectSrcFile) else writeSourceClass(projectSrcFile, source, themePkg, themeClass) } private fun writeSourceClass(classPath: File, source: ThemeSourceData, themePkg: String, themeClass: String) { fun factoryClassText(): String { return when (source) { is ThemeSourceData.SingleLang -> { """class ${source.className} : $themeClass("${source.name}", "${source.baseUrl}", "${source.lang}")""" } is ThemeSourceData.MultiLang -> { val sourceClasses = source.langs.map { lang -> """$themeClass("${source.name}", "${source.baseUrl}", "$lang")""" } """ class ${source.className} : SourceFactory { override fun createSources() = listOf( ${sourceClasses.joinToString(",\n")} ) } """.trimIndent() } } } File("$classPath/${source.className}.kt").writeText( """/* ktlint-disable */ // THIS FILE IS AUTO-GENERATED; DO NOT EDIT package eu.kanade.tachiyomi.extension.${pkgNameSuffix(source, ".")} import eu.kanade.tachiyomi.multisrc.$themePkg.$themeClass ${if (source is ThemeSourceData.MultiLang) "import eu.kanade.tachiyomi.source.SourceFactory" else ""} ${factoryClassText()} """.trimIndent() ) } private fun cleanDirectory(dir: File) { dir.listFiles()?.forEach { file -> if (file.isDirectory) cleanDirectory(file) file.delete() } } } } sealed class ThemeSourceData { abstract val name: String abstract val baseUrl: String abstract val isNsfw: Boolean abstract val className: String abstract val pkgName: String /** * overrideVersionCode defaults to 0, if a source changes their source override code or * a previous existing source suddenly needs source code overrides, overrideVersionCode * should be increased. * When a new source is added with overrides, overrideVersionCode should still be set to 0 * * Note: source code overrides are located in "multisrc/overrides/src//" */ abstract val overrideVersionCode: Int data class SingleLang( override val name: String, override val baseUrl: String, val lang: String, override val isNsfw: Boolean = false, override val className: String = name.replace(" ", ""), override val pkgName: String = className.toLowerCase(Locale.ENGLISH), override val overrideVersionCode: Int = 0, ) : ThemeSourceData() data class MultiLang( override val name: String, override val baseUrl: String, val langs: List, override val isNsfw: Boolean = false, override val className: String = name.replace(" ", "") + "Factory", override val pkgName: String = className.substringBefore("Factory").toLowerCase(Locale.ENGLISH), override val overrideVersionCode: Int = 0, ) : ThemeSourceData() } /** * This variable should be increased when the multisrc library changes in a way that prompts global extension upgrade */ const val multisrcLibraryVersion = 0