From 9194e31208e0bef64f36624e4ee13f6e6ac58398 Mon Sep 17 00:00:00 2001 From: KenjieDec <65448230+KenjieDec@users.noreply.github.com> Date: Thu, 13 Nov 2025 02:47:13 +0700 Subject: [PATCH] HDoujin | Added HDoujin (#11548) * Added HDoujin * Update build.gradle Wrong version, unused dependency * Page Filter * Fixed Sort Filter * Apply AwkwardPeak's Suggestion Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- src/all/hdoujin/build.gradle | 8 + .../hdoujin/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2665 bytes .../hdoujin/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1600 bytes .../hdoujin/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3653 bytes .../hdoujin/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5111 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6830 bytes .../tachiyomi/extension/all/hdoujin/Dto.kt | 195 +++++++++ .../extension/all/hdoujin/Filters.kt | 65 +++ .../extension/all/hdoujin/HDoujin.kt | 397 ++++++++++++++++++ .../extension/all/hdoujin/HDoujinFactory.kt | 13 + 10 files changed, 678 insertions(+) create mode 100644 src/all/hdoujin/build.gradle create mode 100644 src/all/hdoujin/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/hdoujin/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/hdoujin/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/hdoujin/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/hdoujin/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/Dto.kt create mode 100644 src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/Filters.kt create mode 100644 src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujin.kt create mode 100644 src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujinFactory.kt diff --git a/src/all/hdoujin/build.gradle b/src/all/hdoujin/build.gradle new file mode 100644 index 000000000..f34ff3dc3 --- /dev/null +++ b/src/all/hdoujin/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'HDoujin' + extClass = '.HDoujinFactory' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/hdoujin/res/mipmap-hdpi/ic_launcher.png b/src/all/hdoujin/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cd413fd6354bd2a7eb1bcf4d9603f700d60b8276 GIT binary patch literal 2665 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 literal 0 HcmV?d00001 diff --git a/src/all/hdoujin/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/hdoujin/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5beac0505437904b431fef35b93239d1ac00860c GIT binary patch literal 6830 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 o^w_{5qzpi2s@Sr&|G$nXN;?Ge-XQ = emptyList(), +) { + @Serializable + class Tag( + val name: String, + val namespace: Int = 0, + ) + + @Serializable + class Thumbnail( + val path: String, + ) + + @Serializable + class Thumbnails( + val base: String, + val main: Thumbnail, + val entries: List, + ) + fun toSManga() = SManga.create().apply { + val artists = mutableListOf() + val circles = mutableListOf() + val parodies = mutableListOf() + val characters = mutableListOf() + val females = mutableListOf() + val males = mutableListOf() + val mixed = mutableListOf() + val language = mutableListOf() + val other = mutableListOf() + val uploaders = mutableListOf() + val tags = mutableListOf() + this@MangaDetail.tags.forEach { tag -> + when (tag.namespace) { + 1 -> artists.add(tag.name) + 2 -> circles.add(tag.name) + 3 -> parodies.add(tag.name) + 5 -> characters.add(tag.name) + 7 -> tag.name.takeIf { it != "anonymous" }?.let { uploaders.add(it) } + 8 -> males.add(tag.name + " ♂") + 9 -> females.add(tag.name + " ♀") + 10 -> mixed.add(tag.name) + 11 -> language.add(tag.name) + 12 -> other.add(tag.name) + else -> tags.add(tag.name) + } + } + + var appended = false + fun List.joinAndCapitalizeEach(): String? = this.emptyToNull()?.joinToString { it.capitalizeEach() }?.apply { appended = true } + + thumbnail_url = thumbnails.base + thumbnails.main.path + + author = (circles.emptyToNull() ?: artists).joinToString { it.capitalizeEach() } + artist = artists.joinToString { it.capitalizeEach() } + genre = (artists + circles + parodies + characters + tags + females + males + mixed + other).joinToString { it.capitalizeEach() } + description = buildString { + circles.joinAndCapitalizeEach()?.let { + append("Circles: ", it, "\n") + } + uploaders.joinAndCapitalizeEach()?.let { + append("Uploaders: ", it, "\n") + } + parodies.joinAndCapitalizeEach()?.let { + append("Parodies: ", it, "\n") + } + characters.joinAndCapitalizeEach()?.let { + append("Characters: ", it, "\n") + } + + if (appended) append("\n") + + try { + append("Posted: ", dateFormat.format(created_at), "\n") + } catch (_: Exception) {} + + append("Pages: ", thumbnails.entries.size, "\n\n") + + if (!subtitle.isNullOrBlank() || !subtitle_short.isNullOrBlank()) { + append("Alternative Title(s): ", mutableSetOf(subtitle, subtitle_short).filter { !it.isNullOrBlank() }.joinToString { "\n- $it" }, "\n\n") + } + } + status = SManga.COMPLETED + update_strategy = UpdateStrategy.ONLY_FETCH_ONCE + initialized = true + } + + private fun String.capitalizeEach() = this.split(" ").joinToString(" ") { s -> + s.replaceFirstChar { sr -> + if (sr.isLowerCase()) sr.titlecase(Locale.getDefault()) else sr.toString() + } + } + + private fun Collection.emptyToNull(): Collection? { + return this.ifEmpty { null } + } +} + +@Serializable +class Data( + val `0`: DataKey, + val `780`: DataKey? = null, + val `980`: DataKey? = null, + val `1280`: DataKey? = null, + val `1600`: DataKey? = null, +) + +@Serializable +class DataKey( + val id: Int? = null, + val size: Double = 0.0, + val key: String? = null, +) { + fun readableSize() = when { + size >= 300 * 1000 * 1000 -> "${"%.2f".format(size / (1000.0 * 1000.0 * 1000.0))} GB" + size >= 100 * 1000 -> "${"%.2f".format(size / (1000.0 * 1000.0))} MB" + size >= 1000 -> "${"%.2f".format(size / (1000.0))} kB" + else -> "$size B" + } +} + +@Serializable +class MangaData( + val data: Data, +) { + fun size(quality: String): String { + val dataKey = when (quality) { + "1600" -> data.`1600` ?: data.`1280` ?: data.`0` + "1280" -> data.`1280` ?: data.`1600` ?: data.`0` + "980" -> data.`980` ?: data.`1280` ?: data.`0` + "780" -> data.`780` ?: data.`980` ?: data.`0` + else -> data.`0` + } + return dataKey.readableSize() + } +} + +@Serializable +class Entries( + val entries: List, + val limit: Int, + val page: Int, + val total: Int, +) { + @Serializable + class Entry( + val id: Int, + val key: String, + val title: String, + val subtitle: String?, + val thumbnail: Thumbnail, + ) { + fun toSManga() = SManga.create().apply { + url = "$id/$key" + title = this@Entry.title + thumbnail_url = thumbnail.path + } + } + + @Serializable + class Thumbnail( + val path: String, + ) +} + +@Serializable +class ImagesInfo( + val base: String, + val entries: List, +) + +@Serializable +class ImagePath( + val path: String, +) diff --git a/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/Filters.kt b/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/Filters.kt new file mode 100644 index 000000000..98daa4354 --- /dev/null +++ b/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/Filters.kt @@ -0,0 +1,65 @@ + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +fun getFilters(): FilterList { + return FilterList( + SelectFilter("Sort by", getSortsList), + CategoryFilter("Categories"), + Filter.Separator(), + TagType("Tags Include Type", "i"), + TagType("Tags Exclude Type", "e"), + Filter.Separator(), + Filter.Header("Separate tags with commas (,)"), + Filter.Header("Prepend with dash (-) to exclude"), + TextFilter("Tags", "tag"), + TextFilter("Male Tags", "male"), + TextFilter("Female Tags", "female"), + TextFilter("Mixed Tags", "mixed"), + TextFilter("Other Tags", "other"), + Filter.Separator(), + TextFilter("Artists", "artist"), + TextFilter("Parodies", "parody"), + TextFilter("Characters", "character"), + Filter.Separator(), + TextFilter("Uploader", "reason"), + TextFilter("Circles", "circle"), + TextFilter("Languages", "language"), + Filter.Separator(), + Filter.Header("Filter by pages, for example: (>20)"), + TextFilter("Pages", "pages"), + ) +} + +class CheckBoxFilter(name: String, val value: Int, state: Boolean) : Filter.CheckBox(name, state) + +internal class CategoryFilter(name: String) : + Filter.Group( + name, + listOf( + Pair("Manga", 2), + Pair("Doujinshi", 4), + Pair("Illustration", 8), + ).map { CheckBoxFilter(it.first, it.second, true) }, + ) + +internal class TagType(title: String, val type: String) : Filter.Select( + title, + arrayOf("AND", "OR"), +) + +internal open class TextFilter(name: String, val type: String) : Filter.Text(name) + +internal open class SelectFilter(name: String, val vals: List>, state: Int = 2) : + Filter.Select(name, vals.map { it.first }.toTypedArray(), state) { + val selected get() = vals[state].second.takeIf { it.isNotEmpty() } +} + +private val getSortsList: List> = listOf( + Pair("Title", "2"), + Pair("Pages", "3"), + Pair("Date", ""), + Pair("Views", "8"), + Pair("Favourites", "9"), + Pair("Popular This Week", "popular"), +) diff --git a/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujin.kt b/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujin.kt new file mode 100644 index 000000000..4acf073ac --- /dev/null +++ b/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujin.kt @@ -0,0 +1,397 @@ +package eu.kanade.tachiyomi.extension.all.hdoujin + +import CategoryFilter +import SelectFilter +import TagType +import TextFilter +import android.annotation.SuppressLint +import android.app.Application +import android.os.Handler +import android.os.Looper +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreferenceCompat +import eu.kanade.tachiyomi.extension.all.hdoujin.Entries.Entry +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.model.FilterList +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.source.online.HttpSource +import getFilters +import keiyoushi.utils.getPreferences +import keiyoushi.utils.jsonInstance +import kotlinx.serialization.decodeFromString +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import okio.IOException +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +class HDoujin( + override val lang: String, + private val siteLang: String = lang, +) : HttpSource(), ConfigurableSource { + + override val name = "HDoujin" + + override val supportsLatest = true + private val preferences = getPreferences() + private fun quality() = preferences.getString(PREF_IMAGE_RES, "1280")!! + private fun remadd() = preferences.getBoolean(PREF_REM_ADD, false) + private fun alwaysIncludeTags() = preferences.getString(PREF_INCLUDE_TAGS, "") + private fun alwaysExcludeTags() = preferences.getString(PREF_EXCLUDE_TAGS, "") + private fun getTagsPreference(): String { + val include = alwaysIncludeTags() + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotBlank) + + val exclude = alwaysExcludeTags() + ?.split(",") + ?.map(String::trim) + ?.filter(String::isNotBlank) + ?.map { "-$it" } + + val tags: List = include?.plus(exclude ?: emptyList()) ?: exclude?.plus(include ?: emptyList()) ?: emptyList() + if (tags.isNotEmpty()) { + val tagGroups: Map> = tags + .groupBy { + val tag = it.removePrefix("-") + val parts = tag.split(":", limit = 2) + if (parts.size == 2 && parts[0].isNotBlank()) parts[0] else "tag" + } + .mapValues { (_, values) -> + values.mapTo(mutableSetOf()) { + val tag = it.removePrefix("-").split(":").last().trim() + if (it.startsWith("-")) "-$tag" else tag + } + } + + return tagGroups.entries.joinToString(" ") { (key, values) -> + "$key:\"${values.joinToString(",")}\"" + } + } + return "" + } + + override val baseUrl: String = "https://hdoujin.org" + private val baseApiUrl: String = "https://api.hdoujin.org" + private val bookApiUrl: String = "$baseApiUrl/books" + + override fun headersBuilder() = super.headersBuilder() + .set("Referer", "$baseUrl/") + .set("Origin", baseUrl) + + private val context: Application by injectLazy() + private val handler by lazy { Handler(Looper.getMainLooper()) } + private var _clearance: String? = null + + @SuppressLint("SetJavaScriptEnabled") + fun getClearance(): String? { + _clearance?.also { return it } + val latch = CountDownLatch(1) + handler.post { + val webview = WebView(context) + with(webview.settings) { + javaScriptEnabled = true + domStorageEnabled = true + databaseEnabled = true + blockNetworkImage = true + } + webview.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + view!!.evaluateJavascript("window.localStorage.getItem('clearance')") { clearance -> + webview.stopLoading() + webview.destroy() + _clearance = clearance.takeUnless { it == "null" }?.removeSurrounding("\"") + latch.countDown() + } + } + } + webview.loadDataWithBaseURL("$baseUrl/", " ", "text/html", null, null) + } + latch.await(10, TimeUnit.SECONDS) + return _clearance + } + private val clearanceClient = network.cloudflareClient.newBuilder() + .addInterceptor { chain -> + val request = chain.request() + val url = request.url + val clearance = getClearance() + ?: throw IOException("Open webview to refresh token") + + val newUrl = url.newBuilder() + .setQueryParameter("crt", clearance) + .build() + val newRequest = request.newBuilder() + .url(newUrl) + .build() + + val response = chain.proceed(newRequest) + + if (response.code !in listOf(400, 403)) { + return@addInterceptor response + } + response.close() + _clearance = null + throw IOException("Open webview to refresh token") + } + .rateLimit(3) + .build() + + override fun popularMangaRequest(page: Int): Request = GET( + bookApiUrl.toHttpUrl().newBuilder().apply { + addQueryParameter("sort", "8") + addQueryParameter("page", page.toString()) + + val tags = getTagsPreference() + val terms: MutableList = mutableListOf() + if (lang != "all") terms += "language:\"^$siteLang\"" + if (tags.isNotBlank()) terms += tags + + if (terms.isNotEmpty()) addQueryParameter("s", terms.joinToString(" ")) + }.build(), + headers, + ) + + override fun popularMangaParse(response: Response): MangasPage { + val data = response.parseAs() + + with(data) { + return MangasPage( + mangas = entries.map(Entry::toSManga), + hasNextPage = limit * page < total, + ) + } + } + + override fun latestUpdatesRequest(page: Int) = GET( + bookApiUrl.toHttpUrl().newBuilder().apply { + addQueryParameter("page", page.toString()) + + val tags = getTagsPreference() + val terms: MutableList = mutableListOf() + if (lang != "all") terms += "language:\"^$siteLang\"" + if (tags.isNotBlank()) terms += tags + + if (terms.isNotEmpty()) addQueryParameter("s", terms.joinToString(" ")) + }.build(), + headers, + ) + + override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = bookApiUrl.toHttpUrl().newBuilder().apply { + val terms = mutableListOf(query.trim()) + + if (lang != "all") terms += "language:\"^$siteLang$\"" + filters.forEach { filter -> + when (filter) { + is SelectFilter -> { + val value = filter.selected + if (value == "popular") { + addPathSegment(value) + } else { + addQueryParameter("sort", value) + } + } + + is CategoryFilter -> { + val activeFilter = filter.state.filter { it.state } + if (activeFilter.isNotEmpty()) { + addQueryParameter("cat", activeFilter.sumOf { it.value }.toString()) + } + } + + is TextFilter -> { + if (filter.state.isNotEmpty()) { + val tags = filter.state.split(",").filter(String::isNotBlank).joinToString(",") + if (tags.isNotBlank()) { + terms += "${filter.type}:${if (filter.type == "pages") tags else "\"$tags\""}" + } + } + } + + is TagType -> { + if (filter.state > 0) { + addQueryParameter( + filter.type, + when { + filter.type == "i" && filter.state == 0 -> "" + filter.type == "e" && filter.state == 0 -> "1" + else -> "" + }, + ) + } + } + else -> {} + } + } + if (query.isNotEmpty()) terms.add("title:\"$query\"") + if (terms.isNotEmpty()) addQueryParameter("s", terms.joinToString(" ")) + addQueryParameter("page", page.toString()) + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + override fun getFilterList(): FilterList = getFilters() + + private fun getImagesByMangaData(entry: MangaData, entryId: String, entryKey: String): Pair { + val data = entry.data + fun getIPK( + ori: DataKey?, + alt1: DataKey?, + alt2: DataKey?, + alt3: DataKey?, + alt4: DataKey?, + ): Pair { + return Pair( + ori?.id ?: alt1?.id ?: alt2?.id ?: alt3?.id ?: alt4?.id, + ori?.key ?: alt1?.key ?: alt2?.key ?: alt3?.key ?: alt4?.key, + ) + } + val (id, public_key) = when (quality()) { + "1600" -> getIPK(data.`1600`, data.`1280`, data.`0`, data.`980`, data.`780`) + "1280" -> getIPK(data.`1280`, data.`1600`, data.`0`, data.`980`, data.`780`) + "980" -> getIPK(data.`980`, data.`1280`, data.`0`, data.`1600`, data.`780`) + "780" -> getIPK(data.`780`, data.`980`, data.`0`, data.`1280`, data.`1600`) + else -> getIPK(data.`0`, data.`1600`, data.`1280`, data.`980`, data.`780`) + } + + if (id == null || public_key == null) { + throw Exception("No Images Found") + } + + val realQuality = when (id) { + data.`1600`?.id -> "1600" + data.`1280`?.id -> "1280" + data.`980`?.id -> "980" + data.`780`?.id -> "780" + else -> "0" + } + + val imagesResponse = clearanceClient.newCall(GET("$bookApiUrl/data/$entryId/$entryKey/$id/$public_key/$realQuality", headers)).execute() + val images = imagesResponse.parseAs() to realQuality + return images + } + + private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""") + private fun String.shortenTitle() = replace(shortenTitleRegex, "").trim() + + override fun mangaDetailsRequest(manga: SManga) = + GET("$bookApiUrl/detail/${manga.url}", headers) + override fun mangaDetailsParse(response: Response): SManga { + val mangaDetail = response.parseAs() + with(mangaDetail) { + return toSManga().apply { + setUrlWithoutDomain("${mangaDetail.id}/${mangaDetail.key}") + title = if (remadd()) { + title_short + ?: mangaDetail.title.shortenTitle() + } else { + mangaDetail.title + } + } + } + } + + override fun getMangaUrl(manga: SManga) = "$baseUrl/g/${manga.url}" + override fun chapterListRequest(manga: SManga) = GET("$bookApiUrl/detail/${manga.url}", headers) + override fun chapterListParse(response: Response): List { + val manga = response.parseAs() + return listOf( + SChapter.create().apply { + name = "Chapter" + url = "${manga.id}/${manga.key}" + date_upload = (manga.updated_at ?: manga.created_at) + }, + ) + } + + override fun pageListRequest(chapter: SChapter): Request = + POST("$bookApiUrl/detail/${chapter.url}", headers) + override fun fetchPageList(chapter: SChapter): Observable> { + return clearanceClient.newCall(pageListRequest(chapter)) + .asObservableSuccess() + .map { response -> + pageListParse(response) + } + } + override fun pageListParse(response: Response): List { + val mangaData = response.parseAs() + val url = response.request.url.toString() + val matches = Regex("""/detail/(\d+)/([a-z\d]+)""").find(url) + if (matches == null || matches.groupValues.size < 3) return emptyList() + val imagesInfo = getImagesByMangaData(mangaData, matches.groupValues[1], matches.groupValues[2]) + + return imagesInfo.first.entries.mapIndexed { index, image -> + Page(index, imageUrl = "${imagesInfo.first.base}/${image.path}?w=${imagesInfo.second}") + } + } + + override fun imageRequest(page: Page): Request { + return GET(page.imageUrl!!, headers) + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + private inline fun Response.parseAs(): T { + return jsonInstance.decodeFromString(body.string()) + } + + // Settings + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + key = PREF_IMAGE_RES + title = "Image Resolution" + entries = arrayOf("780x", "980x", "1280x", "1600x", "Original") + entryValues = arrayOf("780", "980", "1280", "1600", "0") + summary = "%s" + setDefaultValue("1280") + }.also(screen::addPreference) + + SwitchPreferenceCompat(screen.context).apply { + key = PREF_REM_ADD + title = "Remove additional information in title" + summary = "Remove anything in brackets from manga titles.\n" + + "Reload manga to apply changes to loaded manga." + setDefaultValue(false) + }.also(screen::addPreference) + + EditTextPreference(screen.context).apply { + key = PREF_INCLUDE_TAGS + title = "Tags to include from browse/search" + summary = "Separate tags with commas (,).\n" + + "Excluding: ${alwaysIncludeTags()}" + }.also(screen::addPreference) + EditTextPreference(screen.context).apply { + key = PREF_EXCLUDE_TAGS + title = "Tags to exclude from browse/search" + summary = "Separate tags with commas (,). Supports tag types (females, male, etc), defaults to 'tag' if not specified.\n" + + "Example: 'ai generated, female:hairy, male:hairy'\n" + + "Excluding: ${alwaysExcludeTags()}" + }.also(screen::addPreference) + } + companion object { + private const val PREF_REM_ADD = "pref_remove_additional" + private const val PREF_IMAGE_RES = "pref_image_quality" + private const val PREF_INCLUDE_TAGS = "pref_include_tags" + private const val PREF_EXCLUDE_TAGS = "pref_exclude_tags" + } +} diff --git a/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujinFactory.kt b/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujinFactory.kt new file mode 100644 index 000000000..6d75a5c48 --- /dev/null +++ b/src/all/hdoujin/src/eu/kanade/tachiyomi/extension/all/hdoujin/HDoujinFactory.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.extension.all.hdoujin + +import eu.kanade.tachiyomi.source.SourceFactory + +class HDoujinFactory : SourceFactory { + override fun createSources() = listOf( + HDoujin("all"), + HDoujin("en", "english"), + HDoujin("ja", "japanese"), + HDoujin("kr", "korean"), + HDoujin("zh", "chinese"), + ) +}