diff --git a/src/en/hwtmanga/AndroidManifest.xml b/src/en/hwtmanga/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/en/hwtmanga/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/en/hwtmanga/build.gradle b/src/en/hwtmanga/build.gradle
new file mode 100644
index 000000000..7ba873c1c
--- /dev/null
+++ b/src/en/hwtmanga/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Hardworking Translations'
+ pkgNameSuffix = 'en.hwtmanga'
+ extClass = '.HWTManga'
+ extVersionCode = 1
+ containsNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/hwtmanga/res/mipmap-hdpi/ic_launcher.png b/src/en/hwtmanga/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..64034b426
Binary files /dev/null and b/src/en/hwtmanga/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/hwtmanga/res/mipmap-mdpi/ic_launcher.png b/src/en/hwtmanga/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..fa3710a6b
Binary files /dev/null and b/src/en/hwtmanga/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/hwtmanga/res/mipmap-xhdpi/ic_launcher.png b/src/en/hwtmanga/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..695aaca1c
Binary files /dev/null and b/src/en/hwtmanga/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/hwtmanga/res/mipmap-xxhdpi/ic_launcher.png b/src/en/hwtmanga/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..5ccceacce
Binary files /dev/null and b/src/en/hwtmanga/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/hwtmanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/hwtmanga/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..247cfd6f4
Binary files /dev/null and b/src/en/hwtmanga/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/hwtmanga/res/web_hi_res_512.png b/src/en/hwtmanga/res/web_hi_res_512.png
new file mode 100644
index 000000000..55ffb5305
Binary files /dev/null and b/src/en/hwtmanga/res/web_hi_res_512.png differ
diff --git a/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTFilters.kt b/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTFilters.kt
new file mode 100644
index 000000000..a096e1c81
--- /dev/null
+++ b/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTFilters.kt
@@ -0,0 +1,132 @@
+package eu.kanade.tachiyomi.extension.en.hwtmanga
+
+import eu.kanade.tachiyomi.source.model.Filter
+
+class Tag(name: String, private val id: String) : Filter.CheckBox(name) {
+ override fun toString() = id
+}
+
+private val tags: List
+ get() = listOf(
+ Tag("Action", "action"),
+ Tag("Adventure", "adventure"),
+ Tag("Comedy", "comedy"),
+ Tag("Cooking", "cooking"),
+ Tag("Drama", "drama"),
+ Tag("Fantasy", "fantasy"),
+ Tag("Horror", "horror"),
+ Tag("Mystery", "mystery"),
+ Tag("Martial Arts", "martialarts"),
+ Tag("Romance", "romance"),
+ Tag("School Life", "school"),
+ Tag("Shoujo", "shoujo"),
+ Tag("Shounen", "shounen"),
+ Tag("Supernatural", "supernatural"),
+ Tag("Sci-fi", "sci-fi"),
+ Tag("Slice of Life", "slice of life"),
+ Tag("Adult", "adult"),
+ Tag("Ancient era", "ancient era"),
+ Tag("Arranged Marriage", "arranged_marriage"),
+ Tag("Age gap", "age_gap"),
+ Tag("Betrayal", "betrayal"),
+ Tag("Clan", "clan"),
+ Tag("Childhood Friends", "childhood_friends"),
+ Tag("Couple", "couple"),
+ Tag("Crime", "crime"),
+ Tag("Cultivation", "cultivation"),
+ Tag("Comic", "comic"),
+ Tag("Delinquent", "delinquent"),
+ Tag("Doujinshi", "doujinshi"),
+ Tag("Ecchi", "ecchi"),
+ Tag("Family", "family"),
+ Tag("Fetishes", "fetish"),
+ Tag("Gender Bender", "gender_bender"),
+ Tag("Gyaru", "gyaru"),
+ Tag("Harem", "harem"),
+ Tag("Historical", "historical"),
+ Tag("Isekai", "isekai"),
+ Tag("Josei", "josei"),
+ Tag("Lolicon", "lolicon"),
+ Tag("Leader or Politician", "leader_politician"),
+ Tag("Mature", "mature"),
+ Tag("Magic", "magic"),
+ Tag("Mangaka", "mangaka"),
+ Tag("Masochist", "masochist"),
+ Tag("Monsters", "monsters"),
+ Tag("Mecha", "mecha"),
+ Tag("Music", "music"),
+ Tag("Medical", "medical"),
+ Tag("Misunderstands", "misunderstands"),
+ Tag("OneShot", "oneshot"),
+ Tag("Public figure", "public figure"),
+ Tag("Psychological", "psychological"),
+ Tag("Powerful Lead Character", "powerful"),
+ Tag("Rushed ending", "rushed end"),
+ Tag("Revenge", "revenge"),
+ Tag("Reverse Harem", "reverse_harem"),
+ Tag("Sadist", "sadist"),
+ Tag("Seinen", "seinen"),
+ Tag("Shotacon", "shotacon"),
+ Tag("Secret Crush", "secret_crush"),
+ Tag("Secret Relationship", "secret_relationship"),
+ Tag("Smart MC", "smart_mc"),
+ Tag("Sports", "sports"),
+ Tag("Smut", "smut"),
+ Tag("Tragedy", "tragedy"),
+ Tag("Tomboy", "tomboy"),
+ Tag("Triangles", "triangles"),
+ Tag("Unusual Pupils", "unusual_pupils"),
+ Tag("Vampires", "vampires"),
+ Tag("Webtoon", "webtoon"),
+ Tag("Work", "work"),
+ Tag("Zombies", "zombies"),
+ Tag("4-Koma", "4koma"),
+ Tag("Manga", "manga"),
+ Tag("Manhwa", "manhwa"),
+ Tag("Manhua", "manhua"),
+ )
+
+class TagFilter(
+ values: List = tags
+) : Filter.Group("Tag Match", values) {
+ override fun toString() =
+ state.filter { it.state }.joinToString(";").ifEmpty { "all;" }
+}
+
+private val states: Array
+ get() = arrayOf("ALL", "Completed", "Ongoing")
+
+class StateFilter(
+ values: Array = states
+) : Filter.Select("State", values) {
+ private val ids = arrayOf("all", "complete", "ongoing")
+
+ override fun toString() = ids[state]
+}
+
+private val orders: Array
+ get() = arrayOf(
+ "A~Z",
+ "Z~A",
+ "Newest",
+ "Oldest",
+ "Most Liked",
+ "Most Viewed",
+ "Most Favourite"
+ )
+
+class OrderFilter(
+ values: Array = orders
+) : Filter.Select("Order By", values) {
+ private val ids = arrayOf(
+ "az",
+ "za",
+ "newest",
+ "oldest",
+ "liked",
+ "viewed",
+ "fav"
+ )
+
+ override fun toString() = ids[state]
+}
diff --git a/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTManga.kt b/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTManga.kt
new file mode 100644
index 000000000..fd0a862ee
--- /dev/null
+++ b/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTManga.kt
@@ -0,0 +1,215 @@
+package eu.kanade.tachiyomi.extension.en.hwtmanga
+
+import eu.kanade.tachiyomi.annotations.Nsfw
+import eu.kanade.tachiyomi.network.POST
+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 kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.jsonObject
+import okhttp3.Cookie
+import okhttp3.CookieJar
+import okhttp3.FormBody
+import okhttp3.HttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+@Nsfw class HWTManga : HttpSource() {
+ override val name = "Hardworking Translations"
+
+ override val baseUrl = "https://www.hwtmanga.com/hwt/"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ override val client = network.client.newBuilder().cookieJar(
+ object : CookieJar {
+ override fun saveFromResponse(url: HttpUrl, cookies: List) {}
+
+ override fun loadForRequest(url: HttpUrl) =
+ listOf(
+ Cookie.Builder()
+ .domain("www.hwtmanga.com")
+ .path("/hwt")
+ .name("PHPSESSID")
+ .value(sessionID)
+ .build(),
+ Cookie.Builder()
+ .domain("www.hwtmanga.com")
+ .path("/")
+ .name("manga_security_id")
+ .value(postID)
+ .build()
+ )
+ }
+ ).build()
+
+ private var postID = ""
+
+ private var sessionID = ""
+
+ private val json by injectLazy()
+
+ override fun latestUpdatesRequest(page: Int) =
+ FormBody.Builder().search(order = "newest", pid = page)
+
+ override fun latestUpdatesParse(response: Response) =
+ searchMangaParse(response)
+
+ override fun popularMangaRequest(page: Int) =
+ FormBody.Builder().search(order = "viewed", pid = page)
+
+ override fun popularMangaParse(response: Response) =
+ searchMangaParse(response)
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
+ FormBody.Builder().search(
+ query = query,
+ pid = page,
+ tags = filters.get("all;"),
+ state = filters.get("all"),
+ order = filters.get("az")
+ )
+
+ override fun searchMangaParse(response: Response) =
+ response.parse>("query").map {
+ SManga.create().apply {
+ title = it.title
+ thumbnail_url = it.cimage
+ url = "?page=manga&vid=${it.postID}"
+ }
+ }.let { MangasPage(it, false) }
+
+ override fun fetchMangaDetails(manga: SManga) =
+ FormBody.Builder().post("GET_MANGA_INFO") {
+ add("scom", "0")
+ add("pageid", "1")
+ add("pid", manga.id)
+ }.let(client::newCall).asObservableSuccess().map { res ->
+ // Session cookie is required to view pages
+ if (sessionID == "") {
+ val request = Request.Builder()
+ .url(baseUrl)
+ .headers(headers)
+ .head().build()
+ client.newCall(request).execute().header("Set-Cookie")?.let {
+ sessionID = Cookie.parse(request.url, it)?.value ?: ""
+ }
+ }
+
+ val info = res.parse("mangaInfo")
+ info.tags[0].value = info.mtag.value
+ manga.title = info.title
+ manga.thumbnail_url = info.cover
+ manga.description = info.desc + "\n\n\n" +
+ info.onames.replace(",", " | ")
+ manga.genre = info.tags.joinToString { it.value!! }
+ manga.status = when (info.statue) {
+ 1 -> SManga.ONGOING
+ else -> SManga.UNKNOWN
+ }
+ manga.initialized = true
+ return@map manga
+ }!!
+
+ override fun chapterListRequest(manga: SManga) =
+ FormBody.Builder().post("GET_CHAPTER_LIST") {
+ add("pageid", "1")
+ add("pid", manga.id)
+ }
+
+ override fun chapterListParse(response: Response) =
+ response.parse("all_data").mapIndexed { idx, ch ->
+ SChapter.create().apply {
+ chapter_number = idx + 1f
+ url = "?page=watch_manga&cid=${ch.fid}&pid=${ch.pid}"
+ date_upload = dateFormat.parse(ch.cdate)?.time ?: 0L
+ name = buildString {
+ append("Chapter %.0f".format(chapter_number))
+ if (ch.name != "-") append(" | ${ch.name}")
+ if (ch.is_locked != "false") append(LOCK)
+ }
+ }
+ }
+
+ override fun pageListRequest(chapter: SChapter) =
+ FormBody.Builder().post("GET_CHA_DATA", "manga_viewer") {
+ val tokens = chapter.tokens
+ postID = tokens[5]
+ add("pageid", "1")
+ add("cid", tokens[3])
+ add("pid", postID)
+ }
+
+ override fun pageListParse(response: Response) =
+ response.parse>("clist")
+ .mapIndexed { idx, page -> Page(idx, "", page.image) }
+
+ override fun getFilterList() =
+ FilterList(TagFilter(), StateFilter(), OrderFilter())
+
+ override fun mangaDetailsParse(response: Response) =
+ throw UnsupportedOperationException("Not used")
+
+ override fun imageUrlParse(response: Response) =
+ throw UnsupportedOperationException("Not used")
+
+ private inline val SManga.id: String
+ get() = url.substringAfterLast('=')
+
+ private inline val SChapter.tokens: List
+ get() = url.split('&', '=')
+
+ private inline val HWTPage.image: String
+ get() = if (base.startsWith("http")) base else baseUrl + base
+
+ private fun FormBody.Builder.post(
+ subpage: String,
+ page: String = "mangaData",
+ block: FormBody.Builder.() -> FormBody.Builder
+ ) = add("page", page).add("subpage", subpage).run {
+ POST(baseUrl + "callback.php", headers, block().build())
+ }
+
+ private fun FormBody.Builder.search(
+ query: String = "",
+ tags: String = "all;",
+ state: String = "all",
+ order: String = "az",
+ pid: Int = 1
+ ) = post("MANGASEARCH") {
+ add("searchbox", query)
+ add("byg", tags)
+ add("bys", state)
+ add("byo", order)
+ add("pid", pid.toString())
+ }
+
+ private inline fun Response.parse(key: String) =
+ body!!.string().let { body ->
+ if ("success" !in body) error(body)
+ json.decodeFromJsonElement(
+ json.parseToJsonElement(body).jsonObject[key]!!
+ )
+ }
+
+ private inline fun FilterList.get(default: String) =
+ find { it is T }?.toString() ?: default
+
+ companion object {
+ private const val LOCK = " \uD83D\uDD12"
+
+ private val dateFormat by lazy {
+ SimpleDateFormat("MMM dd, yyyy", Locale.ROOT)
+ }
+ }
+}
diff --git a/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTModels.kt b/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTModels.kt
new file mode 100644
index 000000000..9b6bf6f29
--- /dev/null
+++ b/src/en/hwtmanga/src/eu/kanade/tachiyomi/extension/en/hwtmanga/HWTModels.kt
@@ -0,0 +1,42 @@
+package eu.kanade.tachiyomi.extension.en.hwtmanga
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class HWTQuery(
+ val cimage: String,
+ val postID: String,
+ val title: String
+)
+
+@Serializable
+data class HWTMangaInfo(
+ val cover: String,
+ val desc: String,
+ val mtag: HWTTag,
+ val onames: String,
+ val statue: Int,
+ val postID: Int,
+ val tags: List,
+ val title: String
+)
+
+@Serializable
+data class HWTTag(var value: String?)
+
+@Serializable
+data class HWTChapterList(
+ private val chapterList: List
+) : List by chapterList
+
+@Serializable
+data class HWTChapter(
+ val fid: String,
+ val pid: String,
+ val name: String,
+ val cdate: String,
+ val is_locked: String
+)
+
+@Serializable
+data class HWTPage(val base: String)