diff --git a/src/en/readm/build.gradle b/src/en/readm/build.gradle new file mode 100644 index 000000000..673ff09d8 --- /dev/null +++ b/src/en/readm/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: ReadM' + pkgNameSuffix = 'en.readm' + extClass = '.ReadM' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/readm/res/mipmap-hdpi/ic_launcher.png b/src/en/readm/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..385f23098 Binary files /dev/null and b/src/en/readm/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/readm/res/mipmap-mdpi/ic_launcher.png b/src/en/readm/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..2e4292925 Binary files /dev/null and b/src/en/readm/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/readm/res/mipmap-xhdpi/ic_launcher.png b/src/en/readm/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..3269366d8 Binary files /dev/null and b/src/en/readm/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/readm/res/mipmap-xxhdpi/ic_launcher.png b/src/en/readm/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..e64366c9f Binary files /dev/null and b/src/en/readm/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/readm/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/readm/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..40fb0c35f Binary files /dev/null and b/src/en/readm/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/readm/res/web_hi_res_512.png b/src/en/readm/res/web_hi_res_512.png new file mode 100644 index 000000000..5a751052c Binary files /dev/null and b/src/en/readm/res/web_hi_res_512.png differ diff --git a/src/en/readm/src/eu/kanade/tachiyomi/extension/en/readm/ReadM.kt b/src/en/readm/src/eu/kanade/tachiyomi/extension/en/readm/ReadM.kt new file mode 100644 index 000000000..f55d232d9 --- /dev/null +++ b/src/en/readm/src/eu/kanade/tachiyomi/extension/en/readm/ReadM.kt @@ -0,0 +1,226 @@ +package eu.kanade.tachiyomi.extension.en.readm + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +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 okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.util.Calendar +import java.util.concurrent.TimeUnit + +class ReadM : ParsedHttpSource() { + + //Info + override val name: String = "ReadM" + override val baseUrl: String = "https://readm.org" + override val lang: String = "en" + override val supportsLatest: Boolean = true + override val client: OkHttpClient = network.cloudflareClient.newBuilder() + .connectTimeout(1, TimeUnit.MINUTES) + .readTimeout(1, TimeUnit.MINUTES) + .retryOnConnectionFailure(true) + .followRedirects(true) + .build() + + //Popular + + override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/popular-manga/$page", headers) + override fun popularMangaNextPageSelector(): String? = "div.pagination a:contains(ยป)" + override fun popularMangaSelector(): String = "div#discover-response li" + override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply { + thumbnail_url = element.select("img").attr("abs:src") + element.select("div.subject-title a").first().apply { + title = this.text().trim() + url = this.attr("href") + } + + } + + //Latest + + override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest-releases/$page", headers) + override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector() + override fun latestUpdatesSelector(): String = "ul.latest-updates li" + override fun latestUpdatesFromElement(element: Element): SManga = SManga.create().apply { + thumbnail_url = element.select("img").attr("data-src") + element.select("a").first().apply { + title = this.text().trim() + url = this.attr("href") + } + } + + //Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val formBody = FormBody.Builder() + .add("manga-name", query) + filters.forEach { filter -> + when (filter) { + is TypeFilter -> formBody.add("type", typeArray[filter.state].second) + is AuthorName -> formBody.add("author-name", filter.state) + is ArtistName -> formBody.add("artist-name", filter.state) + is StatusFilter -> formBody.add("status", statusArray[filter.state].second) + is GenreFilter -> filter.state.forEach { genre -> + if (genre.isExcluded()) formBody.add("exclude[]", genre.id) + if (genre.isIncluded()) formBody.add("include[]", genre.id) + } + } + } + if (filters.isEmpty()) { + formBody + .add("type", "all") + .add("status", "both") + } + val searchHeaders = headers.newBuilder().add("X-Requested-With", "XMLHttpRequest").build() + return POST("$baseUrl/service/advanced_search", searchHeaders, formBody.build()) + } + + + override fun searchMangaNextPageSelector(): String? = null + override fun searchMangaSelector(): String = "div.poster-with-subject" + override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element) + + //Details + + override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply { + thumbnail_url = document.select("img.series-profile-thumb").attr("abs:src") + title = document.select("h1.page-title").text().trim() + author = document.select("span#first_episode a").text().trim() + artist = document.select("span#last_episode a").text().trim() + description = document.select("div.series-summary-wrapper p").text().trim() + genre = document.select("div.series-summary-wrapper div.item a").joinToString(", ") { it.text().trim() } + } + + //Chapters + + override fun chapterListSelector(): String = "div.season_start" + override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply { + name = element.select("a").text() + url = element.select("a").attr("href") + date_upload = parseChapterDate(element.select("td.episode-date").text().trim()) + } + + private fun parseChapterDate(date: String): Long { + val dateWords: List<String> = date.split(" ") + + if (dateWords.size == 2) { + val timeAgo = Integer.parseInt(dateWords[0]) + val calendar = Calendar.getInstance() + + when { + dateWords[1].contains("Minute") -> { + calendar.add(Calendar.MINUTE, -timeAgo) + } + dateWords[1].contains("Hour") -> { + calendar.add(Calendar.HOUR_OF_DAY, -timeAgo) + } + dateWords[1].contains("Day") -> { + calendar.add(Calendar.DAY_OF_YEAR, -timeAgo) + } + dateWords[1].contains("Week") -> { + calendar.add(Calendar.WEEK_OF_YEAR, -timeAgo) + } + dateWords[1].contains("Month") -> { + calendar.add(Calendar.MONTH, -timeAgo) + } + dateWords[1].contains("Year") -> { + calendar.add(Calendar.YEAR, -timeAgo) + } + } + + return calendar.timeInMillis + } + + return 0L + } + + //Pages + + override fun imageUrlParse(document: Document): String = throw Exception("Not Used") + override fun pageListParse(document: Document): List<Page> = document.select("div.ch-images img").mapIndexed { index, element -> + Page(index, "", element.attr("abs:data-src")) + } + + //Filters + + override fun getFilterList(): FilterList = FilterList( + TypeFilter(typeArray), + AuthorName(), + ArtistName(), + StatusFilter(statusArray), + GenreFilter(genreArray()) + ) + + private class TypeFilter(values: Array<Pair<String, String>>) : Filter.Select<String>("Type", values.map { it.first }.toTypedArray()) + private class AuthorName : Filter.Text("Author Name") + private class ArtistName : Filter.Text("Artist Name") + private class StatusFilter(values: Array<Pair<String, String>>) : Filter.Select<String>("Status", values.map { it.first }.toTypedArray()) + private class GenreFilter(state: List<Tag>) : Filter.Group<Tag>("Genres", state) + private class Tag(name: String, val id: String) : Filter.TriState(name) + + + private val typeArray = arrayOf( + Pair("All", "all"), + Pair("Japanese Manga", "japanese"), + Pair("Korean Manhwa", "korean"), + Pair("Chinese Manhua", "chinese") + ) + + private val statusArray = arrayOf( + Pair("Both", "both"), + Pair("Ongoing", "ongoing"), + Pair("Completed", "completed") + ) + + private fun genreArray() = listOf( + Tag("Action", "1"), + Tag("Adventure", "23"), + Tag("Comedy", "12"), + Tag("Doujinshi", "26"), + Tag("Drama", "9"), + Tag("Ecchi", "2"), + Tag("Fantasy", "3"), + Tag("Gender Bender", "30"), + Tag("Harem", "4"), + Tag("Historical", "36"), + Tag("Horror", "34"), + Tag("Josei", "17"), + Tag("Lolicon", "39"), + Tag("Manga", "5"), + Tag("Manhua", "31"), + Tag("Manhwa", "32"), + Tag("Martial Arts", "22"), + Tag("Mecha", "33"), + Tag("Mystery", "13"), + Tag("None", "41"), + Tag("One shot", "16"), + Tag("Psychological", "14"), + Tag("Romance", "6"), + Tag("School Life", "10"), + Tag("Sci fi", "19"), + Tag("Sci-fi", "40"), + Tag("Seinen", "24"), + Tag("Shotacon", "38"), + Tag("Shoujo", "8"), + Tag("Shoujo Ai", "37"), + Tag("Shounen", "7"), + Tag("Shounen Ai", "35"), + Tag("Slice of Life", "21"), + Tag("Sports", "29"), + Tag("Supernatural", "11"), + Tag("Tragedy", "15"), + Tag("Uncategorized", "43"), + Tag("Yaoi", "28"), + Tag("Yuri", "20") + ).sortedWith(compareBy { it.name }) + + +}