diff --git a/src/zh/yimmh/AndroidManifest.xml b/src/zh/yimmh/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/zh/yimmh/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/zh/yimmh/build.gradle b/src/zh/yimmh/build.gradle
new file mode 100644
index 000000000..b5736e229
--- /dev/null
+++ b/src/zh/yimmh/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Yimmh'
+ pkgNameSuffix = 'zh.yimmh'
+ extClass = '.Yimmh'
+ extVersionCode = 1
+ libVersion = '1.2'
+}
+
+apply from: "$rootDir/common.gradle"
+
diff --git a/src/zh/yimmh/res/mipmap-hdpi/ic_launcher.png b/src/zh/yimmh/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..951a4eeca
Binary files /dev/null and b/src/zh/yimmh/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/zh/yimmh/res/mipmap-mdpi/ic_launcher.png b/src/zh/yimmh/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..2365b1b92
Binary files /dev/null and b/src/zh/yimmh/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/zh/yimmh/res/mipmap-xhdpi/ic_launcher.png b/src/zh/yimmh/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..1157efd42
Binary files /dev/null and b/src/zh/yimmh/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/zh/yimmh/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/yimmh/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..caf915b49
Binary files /dev/null and b/src/zh/yimmh/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/zh/yimmh/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/yimmh/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..feb2961c9
Binary files /dev/null and b/src/zh/yimmh/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/zh/yimmh/res/web_hi_res_512.png b/src/zh/yimmh/res/web_hi_res_512.png
new file mode 100644
index 000000000..86019471d
Binary files /dev/null and b/src/zh/yimmh/res/web_hi_res_512.png differ
diff --git a/src/zh/yimmh/src/eu/kanade/tachiyomi/extension/zh/yimmh/Yimmh.kt b/src/zh/yimmh/src/eu/kanade/tachiyomi/extension/zh/yimmh/Yimmh.kt
new file mode 100644
index 000000000..d51bb651b
--- /dev/null
+++ b/src/zh/yimmh/src/eu/kanade/tachiyomi/extension/zh/yimmh/Yimmh.kt
@@ -0,0 +1,189 @@
+package eu.kanade.tachiyomi.extension.zh.yimmh
+
+import android.net.Uri
+import eu.kanade.tachiyomi.network.GET
+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.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.Headers
+import okhttp3.Request
+import okhttp3.Response
+import org.json.JSONObject
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+
+class Yimmh : ParsedHttpSource() {
+ override val name: String = "忆漫"
+ override val lang: String = "zh"
+ override val supportsLatest: Boolean = true
+ override val baseUrl: String = "https://m.yimmh.com"
+ override fun headersBuilder() = Headers.Builder()
+ .add("User-Agent", "Mozilla/5.0 (Android 11; Mobile; rv:83.0) Gecko/83.0 Firefox/83.0")
+
+ // Popular
+
+ override fun popularMangaRequest(page: Int) = GET("$baseUrl/rank", headers)
+ override fun popularMangaNextPageSelector(): String? = null
+ override fun popularMangaSelector(): String = "ul.rank-list > a"
+ override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.select("p.rank-list-info-right-title").text()
+ setUrlWithoutDomain(element.attr("abs:href"))
+ thumbnail_url = element.select("img").attr("data-original")
+ }
+
+ // Latest
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ val body = response.body?.string().orEmpty()
+ val json = JSONObject(body)
+ if (json.getInt("err") != 0) {
+ return MangasPage(listOf(), false)
+ }
+ val mangas = arrayListOf()
+ val books = json.getJSONArray("books")
+ for (i in 0 until books.length()) {
+ val book = books.getJSONObject(i)
+ mangas.add(
+ SManga.create().apply {
+ title = book.getString("book_name")
+ url = "/book/${book.getString("unique_id")}"
+ thumbnail_url = book.getString("cover_url")
+ }
+ )
+ }
+ return MangasPage(mangas, true)
+ }
+
+ override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/getUpdate?page=${page * 15 - 15}", headers)
+ override fun latestUpdatesNextPageSelector(): String? = throw Exception("Not used")
+ override fun latestUpdatesSelector() = throw Exception("Not used")
+ override fun latestUpdatesFromElement(element: Element) = throw Exception("Not used")
+
+ // Search
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val uri = Uri.parse(baseUrl).buildUpon()
+ if (query.isNotBlank()) {
+ uri.appendPath("search")
+ .appendQueryParameter("keyword", query)
+ } else {
+ uri.appendPath("getBooks")
+ filters.forEach {
+ if (it is CategoryFilter)
+ uri.appendQueryParameter("tag", it.toUri())
+ else if (it is StatusFilter)
+ uri.appendQueryParameter("end", it.toUri())
+ }
+ uri.appendQueryParameter("page", (page * 15 - 15).toString())
+ }
+ return GET(uri.toString(), headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ return if (response.request.url.toString().startsWith("https://m.yimmh.com/search"))
+ super.searchMangaParse(response)
+ else
+ latestUpdatesParse(response)
+ }
+
+ override fun searchMangaNextPageSelector(): String? = null
+ override fun searchMangaSelector(): String = "ul.book-list > li"
+ override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
+ title = element.select("p.book-list-info-title").text()
+ setUrlWithoutDomain(element.select("a").attr("abs:href"))
+ thumbnail_url = element.select("img").attr("data-original")
+ }
+
+ // Details
+
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ title = document.select("p.detail-main-info-title").text()
+ thumbnail_url = document.select("div.detail-main-cover > img").attr("data-original")
+ author = document.select("p.detail-main-info-author:contains(作者:) > a").text()
+ artist = author
+ genre = document.select("p.detail-main-info-class > span").eachText().joinToString(", ")
+ description = document.select("p.detail-desc").text()
+ status = when (document.select("span.detail-list-title-1").text()) {
+ "连载中" -> SManga.ONGOING
+ "已完结" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ }
+
+ // Chapters
+
+ override fun chapterListSelector(): String = "ul#detail-list-select > li"
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ setUrlWithoutDomain(element.select("a.chapteritem").attr("abs:href"))
+ name = element.select("a.chapteritem").text()
+ }
+ override fun chapterListParse(response: Response): List {
+ return super.chapterListParse(response).reversed()
+ }
+
+ // Pages
+
+ override fun pageListParse(document: Document): List = mutableListOf().apply {
+ var page = document
+ while (true) {
+ val images = page.select("div#cp_img > img.lazy")
+ images.forEach {
+ add(Page(size, "", it.attr("data-src")))
+ }
+ val nextPage = page.select("a.view-bottom-bar-item:contains(下一页)").attr("href")
+ if (nextPage.isNullOrEmpty()) {
+ break
+ } else {
+ page = client.newCall(GET(baseUrl + nextPage, headers))
+ .execute().asJsoup()
+ }
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String = throw Exception("Not Used")
+
+ // Filters
+
+ override fun getFilterList() = FilterList(
+ Filter.Header("如果使用文本搜索"),
+ Filter.Header("过滤器将被忽略"),
+ CategoryFilter(),
+ StatusFilter()
+ )
+
+ private class CategoryFilter : UriSelectFilterPath(
+ "分类",
+ arrayOf(
+ Pair("全部", "全部"),
+ Pair("耽美", "耽美"),
+ Pair("热血", "热血"),
+ Pair("大女主", "大女主")
+ )
+ )
+
+ private class StatusFilter : UriSelectFilterPath(
+ "进度",
+ arrayOf(
+ Pair("-1", "全部"),
+ Pair("1", "连载中"),
+ Pair("2", "已完结")
+ )
+ )
+
+ /**
+ * Class that creates a select filter. Each entry in the dropdown has a name and a display name.
+ * If an entry is selected it is appended as a query parameter onto the end of the URI.
+ */
+ // vals:
+ private open class UriSelectFilterPath(
+ displayName: String,
+ val vals: Array>
+ ) : Filter.Select(displayName, vals.map { it.second }.toTypedArray()) {
+ fun toUri() = vals[state].first
+ }
+}