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)