diff --git a/src/en/wecomics/AndroidManifest.xml b/src/en/wecomics/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/en/wecomics/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest package="eu.kanade.tachiyomi.extension" />
diff --git a/src/en/wecomics/build.gradle b/src/en/wecomics/build.gradle
new file mode 100644
index 000000000..cea83453f
--- /dev/null
+++ b/src/en/wecomics/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+    extName = 'WeComics'
+    pkgNameSuffix = 'en.wecomics'
+    extClass = '.WeComics'
+    extVersionCode = 1
+    libVersion = '1.2'
+}
+
+dependencies {
+    implementation 'org.xxtea:xxtea-java:1.0.5'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/wecomics/res/mipmap-hdpi/ic_launcher.png b/src/en/wecomics/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..2148d01ec
Binary files /dev/null and b/src/en/wecomics/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/wecomics/res/mipmap-mdpi/ic_launcher.png b/src/en/wecomics/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..63305b1c9
Binary files /dev/null and b/src/en/wecomics/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/wecomics/res/mipmap-xhdpi/ic_launcher.png b/src/en/wecomics/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..06e2936c0
Binary files /dev/null and b/src/en/wecomics/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/wecomics/res/mipmap-xxhdpi/ic_launcher.png b/src/en/wecomics/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..650a571a5
Binary files /dev/null and b/src/en/wecomics/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/wecomics/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/wecomics/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6eb69c424
Binary files /dev/null and b/src/en/wecomics/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/wecomics/src/eu/kanade/tachiyomi/extension/en/wecomics/WeComics.kt b/src/en/wecomics/src/eu/kanade/tachiyomi/extension/en/wecomics/WeComics.kt
new file mode 100644
index 000000000..0c91dcd8e
--- /dev/null
+++ b/src/en/wecomics/src/eu/kanade/tachiyomi/extension/en/wecomics/WeComics.kt
@@ -0,0 +1,188 @@
+package eu.kanade.tachiyomi.extension.en.wecomics
+
+import android.util.Base64
+import com.github.salomonbrys.kotson.fromJson
+import com.github.salomonbrys.kotson.get
+import com.google.gson.Gson
+import com.google.gson.JsonObject
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+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 okhttp3.Request
+import okhttp3.Response
+import org.xxtea.XXTEA
+import rx.Observable
+import java.net.URLEncoder
+
+class WeComics : HttpSource() {
+
+    override val name = "WeComics"
+
+    override val baseUrl = "https://m.wecomics.com"
+
+    override val lang = "en"
+
+    override val supportsLatest = true
+
+    private val gson = Gson()
+
+    private fun getMangaId(url: String): String? =
+        Regex("""^/comic/index/id/\d+\?id=(\d+)""").find(url)?.groupValues?.get(1)
+
+    private fun getChapterId(url: String): Pair<String, String> {
+        val pattern = Regex("""^/chapter/index\?id=(\d+)&cid=(\d+)""")
+        val matches = pattern.find(url)?.groupValues!!
+        return Pair(matches[1], matches[2])
+    }
+
+    private fun Int.toStatus() = when (this) {
+        1 -> SManga.ONGOING
+        2 -> SManga.COMPLETED
+        else -> SManga.UNKNOWN
+    }
+
+    // Popular
+
+    override fun popularMangaRequest(page: Int): Request =
+        GET("$baseUrl/h5/rank/getAllComicList/page/$page?plain=1")
+
+    override fun popularMangaParse(response: Response): MangasPage {
+        val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string())
+
+        val mangas = jsonObject["data"]["comic_list"].asJsonArray.map {
+            SManga.create().apply {
+                url = "/comic/index/id/${it["comic_id"].asInt}?id=${it["comic_id"].asInt}"
+                title = it["title"].asString
+                author = it["artist_name"][0].asString.split(",,").joinToString()
+                description = it["brief_intrd"].asString
+                genre = it["tag"].asJsonArray.joinToString { it["name"].asString }
+                status = it["finish_state"].asInt.toStatus()
+                thumbnail_url = it["cover_v_url"].asString
+            }
+        }
+        return MangasPage(mangas, jsonObject["data"]["has_next_page"].asInt == 1)
+    }
+
+    // Latest
+
+    override fun latestUpdatesRequest(page: Int): Request =
+        GET("$baseUrl/h5/rank/getNewComicList/page/$page?plain=1", headers)
+
+    override fun latestUpdatesParse(response: Response): MangasPage =
+        popularMangaParse(response)
+
+    // Search
+
+    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+        val queryEncoded = URLEncoder.encode(query, "UTF-8")
+        return GET("$baseUrl/h5/search/smart/word/$queryEncoded?plain=1", headers)
+    }
+
+    override fun searchMangaParse(response: Response): MangasPage {
+        val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string())
+
+        return MangasPage(
+            jsonObject["data"].asJsonArray.map {
+                SManga.create().apply {
+                    url = "/comic/index/id/${it["comic_id"].asInt}?id=${it["comic_id"].asInt}"
+                    title = it["title"].asString
+                    author = it["artist_name"][0].asString.split(",,").joinToString()
+                    status = SManga.UNKNOWN
+                    thumbnail_url = it["cover_v_url"].asString
+                }
+            },
+            false
+        )
+    }
+
+    // Details
+
+    // mangaDetailsRequest is used for WebView
+    override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
+        return client.newCall(chapterListRequest(manga))
+            .asObservableSuccess()
+            .map { response ->
+                mangaDetailsParse(response).apply { initialized = true }
+            }
+    }
+
+    // For WebView
+    override fun mangaDetailsRequest(manga: SManga): Request =
+        GET("${baseUrl}${manga.url}&type=search", headers)
+
+    override fun mangaDetailsParse(response: Response): SManga {
+        val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string())
+
+        val it = jsonObject["data"]["comic"].asJsonObject
+        return SManga.create().apply {
+            url = "/comic/index/id/${it["comic_id"].asInt}?id=${it["comic_id"].asInt}"
+            title = it["title"].asString
+            author = it["artist_name"][0].asString.split(",,").joinToString()
+            description = it["brief_intrd"].asString
+            genre = it["tag"].asJsonArray.joinToString { it["name"].asString }
+            status = it["finish_state"].asInt.toStatus()
+            thumbnail_url = it["cover_v_url"].asString
+        }
+    }
+
+    // Chapters
+
+    override fun chapterListRequest(manga: SManga): Request =
+        GET("https://m.wecomics.com/h5/comic/detail/id/${getMangaId(manga.url)}?plain=1", headers)
+
+    override fun chapterListParse(response: Response): List<SChapter> {
+        val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string())
+        val mangaId = jsonObject["data"]["comic"]["comic_id"].asInt
+
+        return jsonObject["data"]["chapter_list"].asJsonArray.map {
+            SChapter.create().apply {
+                url = "/chapter/index?id=$mangaId&cid=${it["chapter_id"]}"
+                name = it["title"].asString
+                date_upload = it["publish_time"].asLong * 1000
+                chapter_number = it["seq_no"].asFloat
+                if (it["vip_state"].asInt == 2) scanlator = "Premium"
+            }
+        }
+    }
+
+    // Pages
+
+    override fun pageListRequest(chapter: SChapter): Request {
+        val (mangaId, chapterId) = getChapterId(chapter.url)
+        return GET("$baseUrl/h5/comic/getPictureList/id/$mangaId/cid/$chapterId?plain=1", headers)
+    }
+
+    override fun pageListParse(response: Response): List<Page> {
+        val url = response.request().url().toString()
+
+        // Error code 401 when not logged in and data is empty when logged in,
+        // assuming this is populated after a purchase
+        val jsonObject = gson.fromJson<JsonObject>(response.body()!!.string())
+        if (jsonObject["error_code"].asInt != 2 &&
+            jsonObject["data"]["chapter"]["data"].asString != ""
+        )
+            throw Exception("Chapter is currently not available.")
+
+        val data = jsonObject["data"]["chapter"]["data"].asString
+        val key = data.substring(0, 8)
+        val encrypted = Base64.decode(data.substring(8), Base64.DEFAULT)
+        val chData = XXTEA.decryptToString(encrypted, key)
+
+        val jsonObjectInner = gson.fromJson<JsonObject>(chData)
+        val cdnUrl = jsonObjectInner["cdn_base_url"].asString
+
+        // The inner JSON contains a list of parts of files,
+        // the parts appear to be split at a fixed size
+        return jsonObjectInner["picture_list"].asJsonArray.mapIndexed { i, it ->
+            Page(i, url, cdnUrl + it["picture_url"].asString)
+        }
+    }
+
+    override fun imageUrlParse(response: Response): String =
+        throw UnsupportedOperationException("Not used")
+}