diff --git a/src/all/luscious/AndroidManifest.xml b/src/all/luscious/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/all/luscious/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/all/luscious/build.gradle b/src/all/luscious/build.gradle new file mode 100644 index 000000000..27bdf2508 --- /dev/null +++ b/src/all/luscious/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Luscious' + pkgNameSuffix = 'all.luscious' + extClass = '.LusciousFactory' + extVersionCode = 1 + libVersion = '1.2' + containsNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/luscious/res/mipmap-hdpi/ic_launcher.png b/src/all/luscious/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..175516997 Binary files /dev/null and b/src/all/luscious/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/luscious/res/mipmap-mdpi/ic_launcher.png b/src/all/luscious/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..decc185a5 Binary files /dev/null and b/src/all/luscious/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/luscious/res/mipmap-xhdpi/ic_launcher.png b/src/all/luscious/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..bad49b50c Binary files /dev/null and b/src/all/luscious/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/luscious/res/mipmap-xxhdpi/ic_launcher.png b/src/all/luscious/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..655f1aa6f Binary files /dev/null and b/src/all/luscious/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/luscious/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/luscious/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..8c738053d Binary files /dev/null and b/src/all/luscious/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/luscious/res/web_hi_res_512.png b/src/all/luscious/res/web_hi_res_512.png new file mode 100644 index 000000000..67284a1ee Binary files /dev/null and b/src/all/luscious/res/web_hi_res_512.png differ diff --git a/src/all/luscious/src/eu/kanade/tachiyomi/extension/all/luscious/Luscious.kt b/src/all/luscious/src/eu/kanade/tachiyomi/extension/all/luscious/Luscious.kt new file mode 100644 index 000000000..4da0f6a4b --- /dev/null +++ b/src/all/luscious/src/eu/kanade/tachiyomi/extension/all/luscious/Luscious.kt @@ -0,0 +1,569 @@ +package eu.kanade.tachiyomi.extension.all.luscious + +import com.github.salomonbrys.kotson.addProperty +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.get +import com.github.salomonbrys.kotson.set +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.model.Filter +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 eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import rx.Observable +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +class Luscious(override val lang: String, private val lusLang: String) : HttpSource() { + + override val baseUrl: String = "https://www.luscious.net" + override val name: String = "Luscious" + override val supportsLatest: Boolean = true + + private val apiBaseUrl: String = "https://api.luscious.net/graphql/nobatch/" + + private val gson = Gson() + + override val client: OkHttpClient = network.cloudflareClient + + // Common + + private fun buildAlbumListRequestInput(page: Int, filters: FilterList, query: String = ""): JsonObject { + val sortByFilter = filters.findInstance()!! + val albumTypeFilter = filters.findInstance()!! + val interestsFilter = filters.findInstance()!! + val languagesFilter = filters.findInstance()!! + val tagsFilter = filters.findInstance()!! + val genreFilter = filters.findInstance()!! + val contentTypeFilter = filters.findInstance()!! + + return JsonObject().apply { + add( + "input", + JsonObject().apply { + addProperty("display", sortByFilter.selected) + addProperty("page", page) + add( + "filters", + JsonArray().apply { + + if (contentTypeFilter.selected != FILTER_VALUE_IGNORE) + add(contentTypeFilter.toJsonObject("content_id")) + + if (albumTypeFilter.selected != FILTER_VALUE_IGNORE) + add(albumTypeFilter.toJsonObject("album_type")) + + with(interestsFilter) { + if (this.selected.isEmpty()) { + throw Exception("Please select an Interest") + } + add(this.toJsonObject("audience_ids")) + } + + add( + languagesFilter.toJsonObject("language_ids").apply { + set("value", "+$lusLang${get("value").asString}") + } + ) + + if (tagsFilter.anyNotIgnored()) { + add(tagsFilter.toJsonObject("tagged")) + } + + if (genreFilter.anyNotIgnored()) { + add(genreFilter.toJsonObject("genre_ids")) + } + + if (query != "") { + add( + JsonObject().apply { + addProperty("name", "search_query") + addProperty("value", query) + } + ) + } + } + ) + } + ) + } + } + + private fun buildAlbumListRequest(page: Int, filters: FilterList, query: String = ""): Request { + val input = buildAlbumListRequestInput(page, filters, query) + val url = HttpUrl.parse(apiBaseUrl)!!.newBuilder() + .addQueryParameter("operationName", "AlbumList") + .addQueryParameter("query", ALBUM_LIST_REQUEST_GQL) + .addQueryParameter("variables", input.toString()) + .toString() + return GET(url, headers) + } + + private fun parseAlbumListResponse(response: Response): MangasPage { + val data = gson.fromJson(response.body()!!.string()) + with(data["data"]["album"]["list"]) { + return MangasPage( + this["items"].asJsonArray.map { + SManga.create().apply { + url = it["url"].asString + title = it["title"].asString + thumbnail_url = it["cover"]["url"].asString + } + }, + this["info"]["has_next_page"].asBoolean + ) + } + } + + // Latest + + override fun latestUpdatesRequest(page: Int): Request = buildAlbumListRequest(page, getSortFilters(LATEST_DEFAULT_SORT_STATE)) + + override fun latestUpdatesParse(response: Response): MangasPage = parseAlbumListResponse(response) + + // Chapters + + override fun chapterListParse(response: Response): List { + val document = response.asJsoup() + return listOf( + SChapter.create().apply { + url = response.request().url().toString() + name = "Chapter" + date_upload = document.select(".album-info-item:contains(Created:)")?.first()?.ownText()?.trim()?.let { + DATE_FORMATS_WITH_ORDINAL_SUFFIXES.mapNotNull { format -> format.parseOrNull(it) }.firstOrNull()?.time + } ?: 0L + chapter_number = 1f + } + ) + } + + // Pages + + private fun buildAlbumPicturesRequestInput(id: String, page: Int): JsonObject { + return JsonObject().apply { + addProperty( + "input", + JsonObject().apply { + addProperty( + "filters", + JsonArray().apply { + add( + JsonObject().apply { + addProperty("name", "album_id") + addProperty("value", id) + } + ) + } + ) + addProperty("page", page) + } + ) + } + } + + private fun buildAlbumPicturesPageUrl(id: String, page: Int): String { + val input = buildAlbumPicturesRequestInput(id, page) + return HttpUrl.parse(apiBaseUrl)!!.newBuilder() + .addQueryParameter("operationName", "AlbumListOwnPictures") + .addQueryParameter("query", ALBUM_PICTURES_REQUEST_GQL) + .addQueryParameter("variables", input.toString()) + .toString() + } + + private fun parseAlbumPicturesResponse(response: Response): List { + + val id = response.request().url().queryParameter("variables").toString() + .let { gson.fromJson(it)["input"]["filters"].asJsonArray } + .let { it.first { f -> f["name"].asString == "album_id" } } + .let { it["value"].asString } + + val data = gson.fromJson(response.body()!!.string()) + .let { it["data"]["picture"]["list"].asJsonObject } + + return data["items"].asJsonArray.mapIndexed { index, it -> + Page(index, imageUrl = it["url_to_original"].asString) + } + if (data["info"]["total_pages"].asInt > 1) { // get 2nd page onwards + (ITEMS_PER_PAGE until data["info"]["total_items"].asInt).chunked(ITEMS_PER_PAGE).mapIndexed { page, indices -> + indices.map { Page(it, url = buildAlbumPicturesPageUrl(id, page + 2)) } + }.flatten() + } else emptyList() + } + + override fun fetchPageList(chapter: SChapter): Observable> { + val id = chapter.url.substringAfterLast("_").removeSuffix("/") + return client.newCall(GET(buildAlbumPicturesPageUrl(id, 1))) + .asObservableSuccess() + .map { parseAlbumPicturesResponse(it) } + } + + override fun pageListParse(response: Response): List = throw UnsupportedOperationException("Not used") + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used") + + override fun fetchImageUrl(page: Page): Observable { + if (page.imageUrl != null) { + return Observable.just(page.imageUrl) + } + + return client.newCall(GET(page.url, headers)) + .asObservableSuccess() + .map { + val data = gson.fromJson(it.body()!!.string()).let { data -> + data["data"]["picture"]["list"].asJsonObject + } + data["items"].asJsonArray[page.index % 50].asJsonObject["url_to_original"].asString + } + } + + // Details + + private fun parseMangaGenre(document: Document): String { + return listOf( + document.select(".o-tag--secondary").map { it.text().substringBefore("(").trim() }, + document.select(".o-tag:not([href *= /tags/artist])").map { it.text() }, + document.select(".album-info-item:contains(Content:) .o-tag").map { it.text() } + ).flatten().joinToString() + } + + private fun parseMangaDescription(document: Document): String { + val pageCount: String? = ( + document.select(".album-info-item:contains(pictures)").firstOrNull() + ?: document.select(".album-info-item:contains(gifs)").firstOrNull() + )?.text() + + return listOf( + Pair("Description", document.select(".album-description:last-of-type")?.text()), + Pair("Pages", pageCount) + ).let { + it + listOf("Parody", "Character", "Ethnicity") + .map { key -> key to document.select(".o-tag--category:contains($key) .o-tag").joinToString { t -> t.text() } } + }.filter { desc -> !desc.second.isNullOrBlank() } + .joinToString("\n\n") { "${it.first}:\n${it.second}" } + } + + override fun mangaDetailsParse(response: Response): SManga { + val document = response.asJsoup() + return SManga.create().apply { + + artist = document.select(".o-tag--category:contains(Artist:) .o-tag")?.joinToString() { it.text() } + author = artist + + genre = parseMangaGenre(document) + + title = document.select("a[title]").text() + status = when { + title.contains("ongoing", true) -> SManga.ONGOING + else -> SManga.COMPLETED + } + + description = parseMangaDescription(document) + } + } + + // Popular + + override fun popularMangaParse(response: Response): MangasPage = parseAlbumListResponse(response) + + override fun popularMangaRequest(page: Int): Request = buildAlbumListRequest(page, getSortFilters(POPULAR_DEFAULT_SORT_STATE)) + + // Search + + override fun searchMangaParse(response: Response): MangasPage = parseAlbumListResponse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = buildAlbumListRequest( + page, + filters.let { + if (it.isEmpty()) getSortFilters(SEARCH_DEFAULT_SORT_STATE) + else it + }, + query + ) + + class TriStateFilterOption(name: String, val value: String) : Filter.TriState(name) + abstract class TriStateGroupFilter(name: String, options: List) : Filter.Group(name, options) { + val included: List + get() = state.filter { it.isIncluded() }.map { it.value } + + val excluded: List + get() = state.filter { it.isExcluded() }.map { it.value } + + fun anyNotIgnored(): Boolean = state.any { !it.isIgnored() } + + override fun toString(): String = (included.map { "+$it" } + excluded.map { "-$it" }).joinToString("") + } + + private fun Filter<*>.toJsonObject(key: String): JsonObject { + val value = this.toString() + return JsonObject().apply { + addProperty("name", key) + addProperty("value", value) + } + } + + private class TagGroupFilter(filters: List) : TriStateGroupFilter("Tags", filters) + private class GenreGroupFilter(filters: List) : TriStateGroupFilter("Genres", filters) + + class CheckboxFilterOption(name: String, val value: String, default: Boolean = true) : Filter.CheckBox(name, default) + abstract class CheckboxGroupFilter(name: String, options: List) : Filter.Group(name, options) { + val selected: List + get() = state.filter { it.state }.map { it.value } + + override fun toString(): String = selected.joinToString("") { "+$it" } + } + + private class InterestGroupFilter(options: List) : CheckboxGroupFilter("Interests", options) + private class LanguageGroupFilter(options: List) : CheckboxGroupFilter("Languages", options) + + class SelectFilterOption(val name: String, val value: String) + + abstract class SelectFilter(name: String, private val options: List, default: Int = 0) : Filter.Select(name, options.map { it.name }.toTypedArray(), default) { + val selected: String + get() = options[state].value + + override fun toString(): String = selected + } + class SortBySelectFilter(options: List, default: Int) : SelectFilter("Sort By", options, default) + class AlbumTypeSelectFilter(options: List) : SelectFilter("Album Type", options) + class ContentTypeSelectFilter(options: List) : SelectFilter("Content Type", options) + + override fun getFilterList(): FilterList = getSortFilters(POPULAR_DEFAULT_SORT_STATE) + + private fun getSortFilters(sortState: Int) = FilterList( + SortBySelectFilter(getSortFilters(), sortState), + AlbumTypeSelectFilter(getAlbumTypeFilters()), + ContentTypeSelectFilter(getContentTypeFilters()), + InterestGroupFilter(getInterestFilters()), + LanguageGroupFilter(getLanguageFilters()), + TagGroupFilter(getTagFilters()), + GenreGroupFilter(getGenreFilters()) + ) + + fun getSortFilters() = listOf( + SelectFilterOption("Rating - All Time", "rating_all_time"), + SelectFilterOption("Rating - Last 7 Days", "rating_7_days"), + SelectFilterOption("Rating - Last 14 Days", "rating_14_days"), + SelectFilterOption("Rating - Last 30 Days", "rating_30_days"), + SelectFilterOption("Rating - Last 90 Days", "rating_90_days"), + SelectFilterOption("Rating - Last Year", "rating_1_year"), + SelectFilterOption("Rating - Last Year", "rating_1_year"), + SelectFilterOption("Date - Newest First", "date_newest"), + SelectFilterOption("Date - 2020", "date_2020"), + SelectFilterOption("Date - 2019", "date_2019"), + SelectFilterOption("Date - 2018", "date_2018"), + SelectFilterOption("Date - 2017", "date_2017"), + SelectFilterOption("Date - 2016", "date_2016"), + SelectFilterOption("Date - 2015", "date_2015"), + SelectFilterOption("Date - 2014", "date_2014"), + SelectFilterOption("Date - 2013", "date_2013"), + SelectFilterOption("Date - Oldest First", "date_oldest"), + SelectFilterOption("Date - Upcoming", "date_upcoming"), + SelectFilterOption("Date - Trending", "date_trending"), + SelectFilterOption("Date - Featured", "date_featured"), + SelectFilterOption("Date - Last Viewed", "date_last_interaction"), + ) + + fun getAlbumTypeFilters() = listOf( + SelectFilterOption("Manga", "manga"), + SelectFilterOption("All", FILTER_VALUE_IGNORE), + SelectFilterOption("Pictures", "pictures") + ) + + fun getContentTypeFilters() = listOf( + SelectFilterOption("All", FILTER_VALUE_IGNORE), + SelectFilterOption("Hentai", "0"), + SelectFilterOption("Non-Erotic", "5"), + SelectFilterOption("Real People", "6") + ) + + fun getInterestFilters() = listOf( + CheckboxFilterOption("Straight Sex", "1"), + CheckboxFilterOption("Trans x Girl", "10", false), + CheckboxFilterOption("Gay / Yaoi", "2"), + CheckboxFilterOption("Lesbian / Yuri", "3"), + CheckboxFilterOption("Trans", "5"), + CheckboxFilterOption("Solo Girl", "6"), + CheckboxFilterOption("Trans x Trans", "8"), + CheckboxFilterOption("Trans x Guy", "9") + ) + + fun getLanguageFilters() = listOf( + CheckboxFilterOption("English", ENGLISH_LUS_LANG_VAL, false), + CheckboxFilterOption("Japanese", JAPANESE_LUS_LANG_VAL, false), + CheckboxFilterOption("Spanish", SPANISH_LUS_LANG_VAL, false), + CheckboxFilterOption("Italian", ITALIAN_LUS_LANG_VAL, false), + CheckboxFilterOption("German", GERMAN_LUS_LANG_VAL, false), + CheckboxFilterOption("French", FRENCH_LUS_LANG_VAL, false), + CheckboxFilterOption("Chinese", CHINESE_LUS_LANG_VAL, false), + CheckboxFilterOption("Korean", KOREAN_LUS_LANG_VAL, false), + CheckboxFilterOption("Others", OTHERS_LUS_LANG_VAL, false), + CheckboxFilterOption("Portugese", PORTUGESE_LUS_LANG_VAL, false), + CheckboxFilterOption("Thai", THAI_LUS_LANG_VAL, false) + ).filterNot { it.value == lusLang } + + fun getTagFilters() = listOf( + TriStateFilterOption("Big Breasts", "big_breasts"), + TriStateFilterOption("Blowjob", "blowjob"), + TriStateFilterOption("Anal", "anal"), + TriStateFilterOption("Group", "group"), + TriStateFilterOption("Big Ass", "big_ass"), + TriStateFilterOption("Full Color", "full_color"), + TriStateFilterOption("Schoolgirl", "schoolgirl"), + TriStateFilterOption("Rape", "rape"), + TriStateFilterOption("Glasses", "glasses"), + TriStateFilterOption("Nakadashi", "nakadashi"), + TriStateFilterOption("Yuri", "yuri"), + TriStateFilterOption("Paizuri", "paizuri"), + TriStateFilterOption("Ahegao", "ahegao"), + TriStateFilterOption("Group: metart", "group%3A_metart"), + TriStateFilterOption("Brunette", "brunette"), + TriStateFilterOption("Solo", "solo"), + TriStateFilterOption("Blonde", "blonde"), + TriStateFilterOption("Shaved Pussy", "shaved_pussy"), + TriStateFilterOption("Small Breasts", "small_breasts"), + TriStateFilterOption("Cum", "cum"), + TriStateFilterOption("Stockings", "stockings"), + TriStateFilterOption("Yuri", "yuri"), + TriStateFilterOption("Ass", "ass"), + TriStateFilterOption("Creampie", "creampie"), + TriStateFilterOption("Rape", "rape"), + TriStateFilterOption("Oral Sex", "oral_sex"), + TriStateFilterOption("Bondage", "bondage"), + TriStateFilterOption("Futanari", "futanari"), + TriStateFilterOption("Double Penetration", "double_penetration"), + TriStateFilterOption("Threesome", "threesome"), + TriStateFilterOption("Anal Sex", "anal_sex"), + TriStateFilterOption("Big Cock", "big_cock"), + TriStateFilterOption("Straight Sex", "straight_sex"), + TriStateFilterOption("Yaoi", "yaoi") + ) + + fun getGenreFilters() = listOf( + TriStateFilterOption("3D / Digital Art", "25"), + TriStateFilterOption("Amateurs", "20"), + TriStateFilterOption("Artist Collection", "19"), + TriStateFilterOption("Asian Girls", "12"), + TriStateFilterOption("Cosplay", "22"), + TriStateFilterOption("BDSM", "27"), + TriStateFilterOption("Cross-Dressing", "30"), + TriStateFilterOption("Defloration / First Time", "59"), + TriStateFilterOption("Ebony Girls", "32"), + TriStateFilterOption("European Girls", "46"), + TriStateFilterOption("Fantasy / Monster Girls", "10"), + TriStateFilterOption("Fetish", "2"), + TriStateFilterOption("Furries", "8"), + TriStateFilterOption("Futanari", "31"), + TriStateFilterOption("Group Sex", "36"), + TriStateFilterOption("Harem", "56"), + TriStateFilterOption("Humor", "41"), + TriStateFilterOption("Interracial", "28"), + TriStateFilterOption("Kemonomimi / Animal Ears", "39"), + TriStateFilterOption("Latina Girls", "33"), + TriStateFilterOption("Mature", "13"), + TriStateFilterOption("Members: Original Art", "18"), + TriStateFilterOption("Members: Verified Selfies", "21"), + TriStateFilterOption("Military", "48"), + TriStateFilterOption("Mind Control", "34"), + TriStateFilterOption("Monsters & Tentacles", "38"), + TriStateFilterOption("Netorare / Cheating", "40"), + TriStateFilterOption("No Genre Given", "1"), + TriStateFilterOption("Nonconsent / Reluctance", "37"), + TriStateFilterOption("Other Ethnicity Girls", "57"), + TriStateFilterOption("Public Sex", "43"), + TriStateFilterOption("Romance", "42"), + TriStateFilterOption("School / College", "35"), + TriStateFilterOption("Sex Workers", "47"), + TriStateFilterOption("Softcore / Ecchi", "9"), + TriStateFilterOption("Superheroes", "17"), + TriStateFilterOption("Tankobon", "45"), + TriStateFilterOption("TV / Movies", "51"), + TriStateFilterOption("Trans", "14"), + TriStateFilterOption("Video Games", "15"), + TriStateFilterOption("Vintage", "58"), + TriStateFilterOption("Western", "11"), + TriStateFilterOption("Workplace Sex", "50") + ) + + private inline fun Iterable<*>.findInstance() = find { it is T } as? T + + private fun SimpleDateFormat.parseOrNull(string: String): Date? { + return try { + parse(string) + } catch (e: ParseException) { + null + } + } + + companion object { + + private const val ITEMS_PER_PAGE = 50 + + private val ORDINAL_SUFFIXES = listOf("st", "nd", "rd", "th") + private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES = ORDINAL_SUFFIXES.map { + SimpleDateFormat("MMMM dd'$it', yyyy", Locale.US) + } + + const val ENGLISH_LUS_LANG_VAL = "1" + const val JAPANESE_LUS_LANG_VAL = "2" + const val SPANISH_LUS_LANG_VAL = "3" + const val ITALIAN_LUS_LANG_VAL = "4" + const val GERMAN_LUS_LANG_VAL = "5" + const val FRENCH_LUS_LANG_VAL = "6" + const val CHINESE_LUS_LANG_VAL = "8" + const val KOREAN_LUS_LANG_VAL = "9" + const val OTHERS_LUS_LANG_VAL = "99" + const val PORTUGESE_LUS_LANG_VAL = "100" + const val THAI_LUS_LANG_VAL = "101" + + private const val POPULAR_DEFAULT_SORT_STATE = 18 + private const val LATEST_DEFAULT_SORT_STATE = 7 + private const val SEARCH_DEFAULT_SORT_STATE = 18 + + private const val FILTER_VALUE_IGNORE = "" + + private val ALBUM_LIST_REQUEST_GQL = """ + query AlbumList(${'$'}input: AlbumListInput!) { + album { + list(input: ${'$'}input) { + info { + page + has_next_page + } + items + } + } + } + """.replace("\n", " ").replace("\\s+".toRegex(), " ") + + private val ALBUM_PICTURES_REQUEST_GQL = """ + query AlbumListOwnPictures(${'$'}input: PictureListInput!) { + picture { + list(input: ${'$'}input) { + info { + total_items + total_pages + page + has_next_page + } + items { + url_to_original + } + } + } + } + """.replace("\n", " ").replace("\\s+".toRegex(), " ") + } +} diff --git a/src/all/luscious/src/eu/kanade/tachiyomi/extension/all/luscious/LusciousFactory.kt b/src/all/luscious/src/eu/kanade/tachiyomi/extension/all/luscious/LusciousFactory.kt new file mode 100644 index 000000000..3da7223ad --- /dev/null +++ b/src/all/luscious/src/eu/kanade/tachiyomi/extension/all/luscious/LusciousFactory.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.extension.all.luscious + +import eu.kanade.tachiyomi.annotations.Nsfw +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +@Nsfw +class LusciousFactory : SourceFactory { + + override fun createSources(): List = listOf( + Luscious("en", Luscious.ENGLISH_LUS_LANG_VAL), + Luscious("ja", Luscious.JAPANESE_LUS_LANG_VAL), + Luscious("es", Luscious.SPANISH_LUS_LANG_VAL), + Luscious("it", Luscious.ITALIAN_LUS_LANG_VAL), + Luscious("de", Luscious.GERMAN_LUS_LANG_VAL), + Luscious("fr", Luscious.FRENCH_LUS_LANG_VAL), + Luscious("zh", Luscious.CHINESE_LUS_LANG_VAL), + Luscious("ko", Luscious.KOREAN_LUS_LANG_VAL), + Luscious("other", Luscious.OTHERS_LUS_LANG_VAL), + Luscious("pt", Luscious.PORTUGESE_LUS_LANG_VAL), + Luscious("th", Luscious.THAI_LUS_LANG_VAL) + ) +}