diff --git a/src/ja/comicnewtype/AndroidManifest.xml b/src/ja/comicnewtype/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/ja/comicnewtype/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/ja/comicnewtype/build.gradle b/src/ja/comicnewtype/build.gradle new file mode 100644 index 000000000..ebec894ab --- /dev/null +++ b/src/ja/comicnewtype/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Comic Newtype' + pkgNameSuffix = 'ja.comicnewtype' + extClass = '.ComicNewtype' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/comicnewtype/res/mipmap-hdpi/ic_launcher.png b/src/ja/comicnewtype/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..5c32e2cf0 Binary files /dev/null and b/src/ja/comicnewtype/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/comicnewtype/res/mipmap-mdpi/ic_launcher.png b/src/ja/comicnewtype/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..b0e875d98 Binary files /dev/null and b/src/ja/comicnewtype/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/comicnewtype/res/mipmap-xhdpi/ic_launcher.png b/src/ja/comicnewtype/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..732dc2163 Binary files /dev/null and b/src/ja/comicnewtype/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/comicnewtype/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/comicnewtype/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..682dbb20b Binary files /dev/null and b/src/ja/comicnewtype/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/comicnewtype/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/comicnewtype/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..87fd8181e Binary files /dev/null and b/src/ja/comicnewtype/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/comicnewtype/res/web_hi_res_512.png b/src/ja/comicnewtype/res/web_hi_res_512.png new file mode 100644 index 000000000..d8a468e55 Binary files /dev/null and b/src/ja/comicnewtype/res/web_hi_res_512.png differ diff --git a/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtype.kt b/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtype.kt new file mode 100644 index 000000000..d8039f2ac --- /dev/null +++ b/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtype.kt @@ -0,0 +1,128 @@ +package eu.kanade.tachiyomi.extension.ja.comicnewtype + +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.HttpSource +import eu.kanade.tachiyomi.util.asJsoup +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import org.jsoup.select.Evaluator +import java.text.SimpleDateFormat +import java.util.Locale + +class ComicNewtype : HttpSource() { + override val name = "Comic Newtype" + override val lang = "ja" + override val baseUrl = "https://comic.webnewtype.com" + override val supportsLatest = false + + // Latest is disabled because manga list is sorted by update time by default. + // Ranking page has multiple rankings thus hard to do. + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/contents/?refind_search=all", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val document = response.asJsoup().also { it.parseGenres() } + if (document.selectFirst(Evaluator.Class("section__txt--serch")) != null) + return MangasPage(emptyList(), false) + + val list = document.selectFirst(Evaluator.Class("content__col-list--common")) + val mangas = list.children().map { + val eyeCatcher = it.selectFirst(Evaluator.Class("catch__txt")).ownText() + val root = it.selectFirst(Evaluator.Tag("a")) + SManga.create().apply { + url = root.attr("href") + title = root.selectFirst(Evaluator.Class("detail__txt--ttl")).text() + author = root.selectFirst(Evaluator.Class("detail__txt--info")).ownText() + thumbnail_url = baseUrl + root.selectFirst(Evaluator.Tag("img")) + .attr("src").removeSuffix("/w250/") + val genreText = root.selectFirst(Evaluator.Class("detail__txt--label")).ownText() + if (genreText.isNotEmpty()) { + genre = genreText.substring(1).replace("#", ", ") + } + description = eyeCatcher + } + } + return MangasPage(mangas, false) + } + + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("Not used.") + + override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException("Not used.") + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val path = when { + query.isNotBlank() -> "/search/$query/" + else -> filters.genrePath ?: "/contents/" + } + val url = baseUrl.toHttpUrl().newBuilder(path)!!.addQueries(filters).build() + return Request.Builder().url(url).headers(headers).build() + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + + override fun mangaDetailsParse(response: Response) = SManga.create().apply { + val root = response.asJsoup().selectFirst(Evaluator.Class("pc__list--contents")) + title = root.selectFirst(Evaluator.Tag("h1")).ownText() + author = root.selectFirst(Evaluator.Class("contents__info")).ownText() + // This one is horizontal. Prefer the square one from manga list. + // thumbnail_url = baseUrl + root.selectFirst(Evaluator.Class("contents__thumb-comic")) + // .child(0).attr("src").removeSuffix("/w500/") + genre = root.selectFirst(Evaluator.Class("container__link-list--genre-btn")) + ?.run { children().joinToString { it.text() } } + + val updates = root.selectFirst(Evaluator.Class("contents__date--info-comic")) + .textNodes().filterNot { it.isBlank }.joinToString(" || ") { it.text() } + val isCompleted = (updates == "連載終了") + status = if (isCompleted) SManga.COMPLETED else SManga.ONGOING + description = buildString { + if (!isCompleted) append(updates).append("\n\n") + append(root.selectFirst(Evaluator.Class("contents__txt-catch")).ownText()).append("\n\n") + append(root.selectFirst(Evaluator.Class("contents__txt--desc")).ownText()) + } + } + + override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url + "more/1/", headers) + + override fun chapterListParse(response: Response): List { + val jsonObject = Json.parseToJsonElement(response.body!!.string()).jsonObject + val html = jsonObject["html"]!!.jsonPrimitive.content // asserting ["next"] is 0 + return Jsoup.parseBodyFragment(html).body().children().mapNotNull { element -> + val url = element.child(0).attr("href") + if (url[0] != '/') return@mapNotNull null + + val dateEl = element.selectFirst(Evaluator.Class("detail__txt--date")) + val title = element.selectFirst(Evaluator.Tag("h2")).ownText().halfwidthDigits() + val noteEl = element.selectFirst(Evaluator.Class("detail__txt--caution")) + SChapter.create().apply { + this.url = url + name = if (noteEl == null) title else "$title(${noteEl.ownText()})" + dateEl?.let { dateFormat.parse(it.ownText()) }?.let { date_upload = it.time } + } + } + } + + override fun pageListRequest(chapter: SChapter) = GET(baseUrl + chapter.url + "json/", headers) + + override fun pageListParse(response: Response): List = + Json.decodeFromString>(response.body!!.string()).mapIndexed { index, path -> + val newPath = path.removeSuffix("/h1200q75nc/") + Page(index, imageUrl = baseUrl + newPath) + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Not used.") + + override fun getFilterList() = filterList + + private val dateFormat by lazy { SimpleDateFormat("yyyy/M/d", Locale.ENGLISH) } +} diff --git a/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtypeFilters.kt b/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtypeFilters.kt new file mode 100644 index 000000000..ccdb1a299 --- /dev/null +++ b/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtypeFilters.kt @@ -0,0 +1,82 @@ +package eu.kanade.tachiyomi.extension.ja.comicnewtype + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import okhttp3.HttpUrl +import org.jsoup.nodes.Document +import org.jsoup.select.Evaluator + +var genreList: List = emptyList() + +fun Document.parseGenres() { + if (genreList.isNotEmpty()) return + val container = select(Evaluator.Class("container__link-list--genre-btn")).lastOrNull() ?: return + val items = container.children().ifEmpty { return } + val list = ArrayList(items.size + 1).apply { add(Genre("全て", null)) } + genreList = items.mapTo(list) { + val link = it.child(0) + Genre(link.text(), link.attr("href")) + } +} + +val filterList: FilterList + get() { + val list = buildList(5) { + if (genreList.isEmpty()) { + add(Filter.Header("Press 'Reset' to attempt to show the genres")) + } else { + add(Filter.Header("Genre (ignored for text search)")) + add(GenreFilter(genreList)) + } + add(Filter.Separator()) + add(StatusFilter()) + add(SortFilter()) + } + return FilterList(list) + } + +val FilterList.genrePath: String? + get() { + for (filter in this) { + if (filter is GenreFilter) return filter.path + } + return null + } + +fun HttpUrl.Builder.addQueries(filters: FilterList): HttpUrl.Builder { + for (filter in filters) { + if (filter is QueryFilter) filter.addQueryTo(this) + } + return this +} + +class Genre(val name: String, val path: String?) + +class GenreFilter(private val list: List) : + Filter.Select("Genre", list.map { it.name }.toTypedArray()) { + val path get() = list[state].path +} + +abstract class QueryFilter(name: String, values: Array) : + Filter.Select(name, values) { + abstract fun addQueryTo(builder: HttpUrl.Builder) +} + +class StatusFilter : QueryFilter("Status", STATUS_VALUES) { + override fun addQueryTo(builder: HttpUrl.Builder) { + builder.addQueryParameter("refind_search", STATUS_QUERIES[state]) + } +} + +private val STATUS_VALUES = arrayOf("全て", "連載中", "完結") +private val STATUS_QUERIES = arrayOf("all", "now", "fin") + +class SortFilter : QueryFilter("Sort by", SORT_VALUES) { + override fun addQueryTo(builder: HttpUrl.Builder) { + if (state == 0) return + builder.addQueryParameter("btn_sort", SORT_QUERIES[state]) + } +} + +private val SORT_VALUES = arrayOf("更新順", "五十音順") +private val SORT_QUERIES = arrayOf("opendate", "alphabetical") diff --git a/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtypeUtils.kt b/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtypeUtils.kt new file mode 100644 index 000000000..3539f9f99 --- /dev/null +++ b/src/ja/comicnewtype/src/eu/kanade/tachiyomi/extension/ja/comicnewtype/ComicNewtypeUtils.kt @@ -0,0 +1,7 @@ +package eu.kanade.tachiyomi.extension.ja.comicnewtype + +fun String.halfwidthDigits() = buildString(length) { + for (char in this@halfwidthDigits) { + append(if (char in '0'..'9') char - ('0'.code - '0'.code) else char) + } +}