diff --git a/src/all/manga18me/build.gradle b/src/all/manga18me/build.gradle new file mode 100644 index 000000000..59b8f4fff --- /dev/null +++ b/src/all/manga18me/build.gradle @@ -0,0 +1,9 @@ +ext { + extName = 'Manga18Me' + extClass = '.M18MFactory' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" + diff --git a/src/all/manga18me/res/mipmap-hdpi/ic_launcher.png b/src/all/manga18me/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..566ee3d55 Binary files /dev/null and b/src/all/manga18me/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/manga18me/res/mipmap-mdpi/ic_launcher.png b/src/all/manga18me/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..fd08ca6a7 Binary files /dev/null and b/src/all/manga18me/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/manga18me/res/mipmap-xhdpi/ic_launcher.png b/src/all/manga18me/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..c3206829f Binary files /dev/null and b/src/all/manga18me/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/manga18me/res/mipmap-xxhdpi/ic_launcher.png b/src/all/manga18me/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..7e28fedf7 Binary files /dev/null and b/src/all/manga18me/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/manga18me/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/manga18me/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..2980765d5 Binary files /dev/null and b/src/all/manga18me/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/Filters.kt b/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/Filters.kt new file mode 100644 index 000000000..56887ac01 --- /dev/null +++ b/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/Filters.kt @@ -0,0 +1,84 @@ +package eu.kanade.tachiyomi.extension.all.manga18me + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +fun getFilters(): FilterList { + return FilterList( + Filter.Header(name = "The filter is ignored when using text search."), + GenreFilter("Genre", getGenresList), + SortFilter("Sort", getSortsList), + RawFilter("Raw"), + CompletedFilter("Completed"), + ) +} + +/** Filters **/ + +internal class GenreFilter(name: String, genreList: List<Pair<String, String>>, state: Int = 0) : + SelectFilter(name, genreList, state) + +internal class SortFilter(name: String, sortList: List<Pair<String, String>>, state: Int = 0) : + SelectFilter(name, sortList, state) + +internal class CompletedFilter(name: String) : CheckBoxFilter(name) + +internal class RawFilter(name: String) : CheckBoxFilter(name) + +internal open class CheckBoxFilter(name: String, val value: String = "") : Filter.CheckBox(name) + +internal open class SelectFilter(name: String, private val vals: List<Pair<String, String>>, state: Int = 0) : + Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) { + fun getValue() = vals[state].second +} + +/** Filters Data **/ +private val getGenresList: List<Pair<String, String>> = listOf( + Pair("Manga", "manga"), + Pair("Drama", "drama"), + Pair("Mature", "mature"), + Pair("Romance", "romance"), + Pair("Adult", "adult"), + Pair("Hentai", "hentai"), + Pair("Comedy", "comedy"), + Pair("Ecchi", "ecchi"), + Pair("School Life", "school-life"), + Pair("Shounen", "shounen"), + Pair("Slice of Life", "slice-of-life"), + Pair("Seinen", "seinen"), + Pair("Yuri", "yuri"), + Pair("Action", "action"), + Pair("Fantasy", "fantasy"), + Pair("Harem", "harem"), + Pair("Supernatural", "supernatural"), + Pair("Sci-Fi", "sci-fi"), + Pair("Isekai", "isekai"), + Pair("Shoujo", "shoujo"), + Pair("Horror", "horror"), + Pair("Psychological", "psychological"), + Pair("Smut", "smut"), + Pair("Tragedy", "tragedy"), + Pair("Raw", "raw"), + Pair("Historical", "historical"), + Pair("Adventure", "adventure"), + Pair("Martial Arts", "martial-arts"), + Pair("Manhwa", "manhwa"), + Pair("Manhua", "manhua"), + Pair("Mystery", "mystery"), + Pair("BL", "bl"), + Pair("Yaoi", "yaoi"), + Pair("Gender Bender", "gender-bender"), + Pair("Thriller", "thriller"), + Pair("Josei", "josei"), + Pair("Sports", "sports"), + Pair("GL", "gl"), + Pair("Family", "family"), + Pair("Magic", "magic"), +) + +private val getSortsList: List<Pair<String, String>> = listOf( + Pair("Latest", "latest"), + Pair("A-Z", "alphabet"), + Pair("Rating", "rating"), + Pair("Trending", "trending"), +) diff --git a/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/M18MFactory.kt b/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/M18MFactory.kt new file mode 100644 index 000000000..a2c0e2d53 --- /dev/null +++ b/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/M18MFactory.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.extension.all.manga18me + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class M18MFactory : SourceFactory { + override fun createSources(): List<Source> = + listOf( + Manga18Me("all"), + Manga18Me("en"), + ) +} diff --git a/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/Manga18Me.kt b/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/Manga18Me.kt new file mode 100644 index 000000000..bef68bcfc --- /dev/null +++ b/src/all/manga18me/src/eu/kanade/tachiyomi/extension/all/manga18me/Manga18Me.kt @@ -0,0 +1,194 @@ +package eu.kanade.tachiyomi.extension.all.manga18me + +import eu.kanade.tachiyomi.network.GET +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.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.lang.Exception +import java.text.SimpleDateFormat +import java.util.Locale + +open class Manga18Me(override val lang: String) : ParsedHttpSource() { + + override val name = "Manga18.me" + + override val baseUrl = "https://manga18.me" + + override val supportsLatest = true + + override val client = network.cloudflareClient + + override fun headersBuilder() = Headers.Builder().apply { + add("Referer", "$baseUrl/") + } + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/manga/$page?orderby=trending", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup() + + val entries = document.select(popularMangaSelector()) + val hasNextPage = document.selectFirst(popularMangaNextPageSelector()) != null + + if (lang == "en") { + val searchText = document.selectFirst("div.section-heading h1")?.text() ?: "" + val raw = document.selectFirst("div.canonical")?.attr("href") ?: "" + return MangasPage( + entries + .filter { it -> + val title = it.selectFirst("div.item-thumb.wleft a")?.attr("href") ?: "" + + searchText.lowercase().contains("raw") || + raw.contains("raw") || + !title.contains("raw") + } + .map(::popularMangaFromElement), + hasNextPage, + ) + } + + return MangasPage(entries.map(::popularMangaFromElement), hasNextPage) + } + + override fun popularMangaSelector() = "div.page-item-detail" + override fun popularMangaNextPageSelector() = ".next" + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href")) + title = element.selectFirst("div.item-thumb.wleft a")!!.attr("title") + thumbnail_url = element.selectFirst("img")?.absUrl("src") + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/manga/$page?orderby=latest", headers) + } + + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + override fun latestUpdatesSelector() = popularMangaSelector() + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = baseUrl.toHttpUrl().newBuilder().apply { + if (query.isEmpty()) { + var completed = false + var raw = false + var genre = "" + filters.forEach { + when (it) { + is GenreFilter -> { + genre = it.getValue() + } + + is CompletedFilter -> { + completed = it.state + } + + is RawFilter -> { + raw = it.state + } + + is SortFilter -> { + addQueryParameter("orderby", it.getValue()) + } + + else -> {} + } + } + if (raw) { + addPathSegment("raw") + } else if (completed) { + addPathSegment("completed") + } else { + if (genre != "manga") addPathSegment("genre") + addPathSegment(genre) + } + addPathSegment(page.toString()) + } else { + addPathSegment("search") + addQueryParameter("q", query) + addQueryParameter("page", page.toString()) + } + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + override fun searchMangaSelector() = popularMangaSelector() + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + override fun getFilterList() = getFilters() + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val info = document.selectFirst("div.post_content")!! + + title = document.select("div.post-title.wleft > h1").text() + description = buildString { + document.select("div.ss-manga > p") + .eachText().onEach { + append(it.trim()) + append("\n\n") + } + + info.selectFirst("div.post-content_item.wleft:contains(Alternative) div.summary-content") + ?.text() + ?.takeIf { it != "Updating" && it.isNotEmpty() } + ?.let { + append("Alternative Names:\n") + append(it.trim()) + } + } + status = when (info.select("div.post-content_item.wleft:contains(Status) div.summary-content").text()) { + "Ongoing" -> SManga.ONGOING + "Completed" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + author = info.selectFirst("div.href-content.artist-content > a")?.text()?.takeIf { it != "Updating" } + artist = info.selectFirst("div.href-content.artist-content > a")?.text()?.takeIf { it != "Updating" } + genre = info.select("div.href-content.genres-content > a[href*=/manga-list/]").eachText().joinToString() + thumbnail_url = document.selectFirst("div.summary_image > img")?.absUrl("src") + } + + override fun chapterListSelector() = "ul.row-content-chapter.wleft .a-h.wleft" + + private val dateFormat = SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH) + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + element.selectFirst("a")!!.run { + setUrlWithoutDomain(absUrl("href")) + name = text() + } + date_upload = try { + dateFormat.parse(element.selectFirst("span")!!.text())!!.time + } catch (_: Exception) { + 0L + } + } + + override fun pageListParse(document: Document): List<Page> { + val contents = document.select("div.read-content.wleft img") + if (contents.isEmpty()) { + throw Exception("Unable to find script with image data") + } + + return contents.mapIndexed { idx, image -> + val imageUrl = image.attr("src") + Page(idx, imageUrl = imageUrl) + } + } + override fun imageUrlParse(document: Document) = throw UnsupportedOperationException() +}