diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-hdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index 5e33ea934..000000000
Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-mdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index dd62bd9b3..000000000
Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-xhdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index deda02113..000000000
Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 3f4776373..000000000
Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png b/multisrc/overrides/madara/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index 0dbbbf5d8..000000000
Binary files a/multisrc/overrides/madara/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/multisrc/overrides/madara/hentaidexy/res/web_hi_res_512.png b/multisrc/overrides/madara/hentaidexy/res/web_hi_res_512.png
deleted file mode 100644
index a0742ce37..000000000
Binary files a/multisrc/overrides/madara/hentaidexy/res/web_hi_res_512.png and /dev/null differ
diff --git a/multisrc/overrides/madara/hentaidexy/src/Hentaidexy.kt b/multisrc/overrides/madara/hentaidexy/src/Hentaidexy.kt
deleted file mode 100644
index 68ea81e5c..000000000
--- a/multisrc/overrides/madara/hentaidexy/src/Hentaidexy.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package eu.kanade.tachiyomi.extension.en.hentaidexy
-
-import eu.kanade.tachiyomi.multisrc.madara.Madara
-import eu.kanade.tachiyomi.network.interceptor.rateLimit
-import okhttp3.OkHttpClient
-import java.util.concurrent.TimeUnit
-
-class Hentaidexy : Madara("Hentaidexy", "https://hentaidexy.net", "en") {
-
- override val client: OkHttpClient = super.client.newBuilder()
- .rateLimit(1, 1, TimeUnit.SECONDS)
- .build()
-}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
index 1925c392e..178f32c3c 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/madara/MadaraGenerator.kt
@@ -127,7 +127,6 @@ class MadaraGenerator : ThemeSourceGenerator {
SingleLang("Hentai Manga", "https://hentaimanga.me", "en", isNsfw = true, overrideVersionCode = 1),
SingleLang("Hentai Teca", "https://hentaiteca.net", "pt-BR", isNsfw = true, overrideVersionCode = 1),
SingleLang("Hentai20", "https://hentai20.io", "en", isNsfw = true, overrideVersionCode = 3),
- SingleLang("Hentaidexy", "https://hentaidexy.net", "en", isNsfw = true, overrideVersionCode = 3),
SingleLang("HentaiRead", "https://hentairead.com", "en", isNsfw = true, className = "Hentairead", overrideVersionCode = 3),
SingleLang("HentaiWebtoon", "https://hentaiwebtoon.com", "en", isNsfw = true, overrideVersionCode = 1),
SingleLang("HentaiXComic", "https://hentaixcomic.com", "en", isNsfw = true),
diff --git a/src/en/hentaidexy/AndroidManifest.xml b/src/en/hentaidexy/AndroidManifest.xml
new file mode 100644
index 000000000..8f35f8fe2
--- /dev/null
+++ b/src/en/hentaidexy/AndroidManifest.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/en/hentaidexy/build.gradle b/src/en/hentaidexy/build.gradle
new file mode 100644
index 000000000..b92960d2c
--- /dev/null
+++ b/src/en/hentaidexy/build.gradle
@@ -0,0 +1,13 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Hentaidexy'
+ pkgNameSuffix = 'en.hentaidexy'
+ extClass = '.Hentaidexy'
+ extVersionCode = 32
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/hentaidexy/res/mipmap-hdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..39990edf3
Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/hentaidexy/res/mipmap-mdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..3f407bacd
Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/hentaidexy/res/mipmap-xhdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..f8e94a64c
Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..20d8befa1
Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f1f73f010
Binary files /dev/null and b/src/en/hentaidexy/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/hentaidexy/res/web_hi_res_512.png b/src/en/hentaidexy/res/web_hi_res_512.png
new file mode 100644
index 000000000..31fad88c0
Binary files /dev/null and b/src/en/hentaidexy/res/web_hi_res_512.png differ
diff --git a/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/Hentaidexy.kt b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/Hentaidexy.kt
new file mode 100644
index 000000000..ac7cefad8
--- /dev/null
+++ b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/Hentaidexy.kt
@@ -0,0 +1,206 @@
+package eu.kanade.tachiyomi.extension.en.hentaidexy
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
+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.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class Hentaidexy : HttpSource() {
+
+ override val name = "Hentaidexy"
+
+ override val baseUrl = "https://hentaidexy.net"
+
+ private val apiUrl = "https://backend.hentaidexy.net"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ override val versionId = 2
+
+ private val json: Json by injectLazy()
+
+ override val client: OkHttpClient = super.client.newBuilder()
+ .rateLimitHost(apiUrl.toHttpUrl(), 1)
+ .build()
+
+ override fun headersBuilder() = Headers.Builder().apply {
+ add("Referer", "$baseUrl/")
+ }
+
+ // popular
+ override fun popularMangaRequest(page: Int): Request {
+ return GET("$apiUrl/api/v1/mangas?page=$page&limit=100&sort=-views", headers)
+ }
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val result = json.decodeFromString(response.body.string())
+ return MangasPage(
+ result.mangas.map { manga ->
+ toSManga(manga).apply {
+ initialized = true
+ }
+ },
+ hasNextPage = result.totalPages > result.page,
+ )
+ }
+
+ // latest
+ override fun latestUpdatesRequest(page: Int): Request {
+ return GET("$apiUrl/api/v1/mangas?page=$page&limit=100&sort=-updatedAt", headers)
+ }
+
+ override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
+
+ // search
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
+ if (!query.startsWith(ID_SEARCH_PREFIX)) {
+ return super.fetchSearchManga(page, query, filters)
+ }
+
+ val id = query.substringAfter(ID_SEARCH_PREFIX)
+ return fetchMangaDetails(SManga.create().apply { url = id }).map {
+ MangasPage(listOf(it), false)
+ }
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ return GET("$apiUrl/api/v1/mangas?page=$page&altTitles=$query&sort=createdAt", headers)
+ }
+
+ override fun searchMangaParse(response: Response) = popularMangaParse(response)
+
+ // manga details
+ override fun mangaDetailsRequest(manga: SManga): Request {
+ return GET("$apiUrl/api/v1/mangas/${manga.url}", headers)
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val mangaDetails = json.decodeFromString(response.body.string())
+ return toSManga(mangaDetails.manga)
+ }
+
+ override fun getMangaUrl(manga: SManga): String {
+ val mangaId = manga.url
+ val slug = manga.title.replace(' ', '-')
+ return "$baseUrl/manga/$mangaId/$slug"
+ }
+
+ // chapter list
+ override fun chapterListRequest(manga: SManga): Request {
+ return paginatedChapterListRequest(manga.url, 1)
+ }
+
+ private fun paginatedChapterListRequest(mangaID: String, page: Int): Request {
+ return GET("$apiUrl/api/v1/mangas/$mangaID/chapters?sort=-serialNumber&limit=100&page=$page", headers)
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val chapterListResponse = json.decodeFromString(response.body.string())
+
+ val mangaId = response.request.url.toString()
+ .substringAfter("/mangas/")
+ .substringBefore("/chapters")
+
+ val totalPages = chapterListResponse.totalPages
+ var currentPage = 1
+
+ while (totalPages > currentPage) {
+ currentPage++
+ val newRequest = paginatedChapterListRequest(mangaId, currentPage)
+ val newResponse = client.newCall(newRequest).execute()
+ val newChapterListResponse = json.decodeFromString(newResponse.body.string())
+
+ chapterListResponse.chapters += newChapterListResponse.chapters
+ }
+
+ return chapterListResponse.chapters.map { chapter ->
+ SChapter.create().apply {
+ url = "/manga/$mangaId/chapter/${chapter._id}"
+ name = "Chapter " + chapter.serialNumber.parseChapterNumber()
+ date_upload = chapter.createdAt.parseDate()
+ }
+ }
+ }
+
+ override fun getChapterUrl(chapter: SChapter): String {
+ return "$baseUrl${chapter.url}"
+ }
+
+ // page list
+ override fun pageListRequest(chapter: SChapter): Request {
+ val chapterId = chapter.url.substringAfterLast('/')
+ return GET("$apiUrl/api/v1/chapters/$chapterId", headers)
+ }
+
+ override fun pageListParse(response: Response): List {
+ val result = json.decodeFromString(response.body.string())
+ return result.chapter.images.mapIndexed { index, image ->
+ Page(index = index, imageUrl = image)
+ }
+ }
+
+ // unused
+ override fun imageUrlParse(response: Response): String {
+ throw UnsupportedOperationException("Not Used")
+ }
+
+ // Helpers
+ private fun toSManga(manga: Manga): SManga {
+ return SManga.create().apply {
+ url = manga._id
+ title = manga.title
+ author = manga.authors?.joinToString { it.trim() }
+ artist = author
+ description = manga.summary.trim() + "\n\nAlternative Names: ${manga.altTitles?.joinToString { it.trim() }}"
+ genre = manga.genres?.joinToString { it.trim() }
+ status = manga.status.parseStatus()
+ thumbnail_url = manga.coverImage
+ }
+ }
+
+ private fun String.parseStatus(): Int {
+ return when {
+ this.contains("ongoing", true) -> SManga.ONGOING
+ this.contains("complete", true) -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ }
+
+ private fun Float.parseChapterNumber(): String {
+ return if (this.toInt().toFloat() == this) {
+ this.toInt().toString()
+ } else {
+ this.toString()
+ }
+ }
+
+ private fun String.parseDate(): Long {
+ return runCatching { DATE_FORMATTER.parse(this)?.time }
+ .getOrNull() ?: 0L
+ }
+
+ companion object {
+ private val DATE_FORMATTER by lazy {
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
+ }
+
+ const val ID_SEARCH_PREFIX = "id:"
+ }
+}
diff --git a/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyDto.kt b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyDto.kt
new file mode 100644
index 000000000..24a24ac47
--- /dev/null
+++ b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyDto.kt
@@ -0,0 +1,51 @@
+package eu.kanade.tachiyomi.extension.en.hentaidexy
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ApiMangaResponse(
+ val page: Int,
+ val totalPages: Int,
+ val mangas: Array,
+)
+
+@Serializable
+data class Manga(
+ val _id: String,
+ val title: String,
+ val altTitles: Array?,
+ val coverImage: String,
+ val summary: String = "Unknown",
+ val authors: Array?,
+ val genres: Array?,
+ val status: String,
+)
+
+@Serializable
+data class MangaDetails(
+ val manga: Manga,
+)
+
+@Serializable
+data class ApiChapterResponse(
+ val page: Int,
+ val totalPages: Int,
+ val chapters: MutableList,
+)
+
+@Serializable
+data class Chapter(
+ val _id: String,
+ val serialNumber: Float,
+ val createdAt: String,
+)
+
+@Serializable
+data class PageList(
+ val chapter: ChapterPageData,
+)
+
+@Serializable
+data class ChapterPageData(
+ val images: Array,
+)
diff --git a/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyUrlActivity.kt b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyUrlActivity.kt
new file mode 100644
index 000000000..7da54a0fa
--- /dev/null
+++ b/src/en/hentaidexy/src/eu/kanade/tachiyomi/extension/en/hentaidexy/HentaidexyUrlActivity.kt
@@ -0,0 +1,34 @@
+package eu.kanade.tachiyomi.extension.en.hentaidexy
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+class HentaidexyUrlActivity : Activity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+ if (pathSegments != null && pathSegments.size > 1) {
+ val id = pathSegments[1]
+ val mainIntent = Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "${Hentaidexy.ID_SEARCH_PREFIX}$id")
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(mainIntent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e("HentaidexyUrlActivity", e.toString())
+ }
+ } else {
+ Log.e("HentaidexyUrlActivity", "could not parse uri from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}