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)
+ }
+}