diff --git a/src/es/hentaimode/AndroidManifest.xml b/src/es/hentaimode/AndroidManifest.xml
new file mode 100644
index 000000000..58d50e015
--- /dev/null
+++ b/src/es/hentaimode/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/es/hentaimode/build.gradle b/src/es/hentaimode/build.gradle
new file mode 100644
index 000000000..485ba1fa5
--- /dev/null
+++ b/src/es/hentaimode/build.gradle
@@ -0,0 +1,8 @@
+ext {
+ extName = 'HentaiMode'
+ extClass = '.HentaiMode'
+ extVersionCode = 1
+ isNsfw = true
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/es/hentaimode/res/mipmap-hdpi/ic_launcher.png b/src/es/hentaimode/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..9b84415f2
Binary files /dev/null and b/src/es/hentaimode/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/es/hentaimode/res/mipmap-mdpi/ic_launcher.png b/src/es/hentaimode/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..2e9ea50a9
Binary files /dev/null and b/src/es/hentaimode/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/es/hentaimode/res/mipmap-xhdpi/ic_launcher.png b/src/es/hentaimode/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..8474c881d
Binary files /dev/null and b/src/es/hentaimode/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/es/hentaimode/res/mipmap-xxhdpi/ic_launcher.png b/src/es/hentaimode/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..99b291ec8
Binary files /dev/null and b/src/es/hentaimode/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/es/hentaimode/res/mipmap-xxxhdpi/ic_launcher.png b/src/es/hentaimode/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..aac9ed6c2
Binary files /dev/null and b/src/es/hentaimode/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/es/hentaimode/src/eu/kanade/tachiyomi/extension/es/hentaimode/HentaiMode.kt b/src/es/hentaimode/src/eu/kanade/tachiyomi/extension/es/hentaimode/HentaiMode.kt
new file mode 100644
index 000000000..6c456f7ab
--- /dev/null
+++ b/src/es/hentaimode/src/eu/kanade/tachiyomi/extension/es/hentaimode/HentaiMode.kt
@@ -0,0 +1,168 @@
+package eu.kanade.tachiyomi.extension.es.hentaimode
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+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.model.UpdateStrategy
+import eu.kanade.tachiyomi.source.online.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+
+class HentaiMode : ParsedHttpSource() {
+
+ override val name = "HentaiMode"
+
+ override val baseUrl = "https://hentaimode.com"
+
+ override val lang = "es"
+
+ override val supportsLatest = false
+
+ override val client = network.client.newBuilder()
+ .rateLimitHost(baseUrl.toHttpUrl(), 2)
+ .build()
+
+ // ============================== Popular ===============================
+ override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
+
+ override fun popularMangaSelector() = "div.row div.book-list > a"
+
+ override fun popularMangaFromElement(element: Element) = SManga.create().apply {
+ setUrlWithoutDomain(element.absUrl("href"))
+ title = element.selectFirst(".book-description > p")!!.text()
+ thumbnail_url = element.selectFirst("img")?.absUrl("src")
+ }
+
+ override fun popularMangaNextPageSelector() = null
+
+ // =============================== Latest ===============================
+ override fun latestUpdatesRequest(page: Int): Request {
+ throw UnsupportedOperationException()
+ }
+
+ override fun latestUpdatesSelector(): String {
+ throw UnsupportedOperationException()
+ }
+
+ override fun latestUpdatesFromElement(element: Element): SManga {
+ throw UnsupportedOperationException()
+ }
+
+ override fun latestUpdatesNextPageSelector(): String? {
+ throw UnsupportedOperationException()
+ }
+
+ // =============================== Search ===============================
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
+ return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
+ val id = query.removePrefix(PREFIX_SEARCH)
+ client.newCall(GET("$baseUrl/g/$id"))
+ .asObservableSuccess()
+ .map(::searchMangaByIdParse)
+ } else {
+ super.fetchSearchManga(page, query, filters)
+ }
+ }
+
+ private fun searchMangaByIdParse(response: Response): MangasPage {
+ val doc = response.asJsoup()
+ val details = mangaDetailsParse(doc)
+ .apply { setUrlWithoutDomain(doc.location()) }
+ return MangasPage(listOf(details), false)
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ require(query.length >= 3) { "Please use at least 3 characters!" }
+ return GET("$baseUrl/buscar?s=$query")
+ }
+
+ override fun searchMangaSelector() = popularMangaSelector()
+
+ override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
+
+ override fun searchMangaNextPageSelector() = null
+
+ // =========================== Manga Details ============================
+ private val additionalInfos = listOf("Serie", "Tipo", "Personajes", "Idioma")
+
+ override fun mangaDetailsParse(document: Document) = SManga.create().apply {
+ thumbnail_url = document.selectFirst("div#cover img")?.absUrl("src")
+ status = SManga.COMPLETED
+ update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
+ with(document.selectFirst("div#info-block > div#info")!!) {
+ title = selectFirst("h1")!!.text()
+ genre = getInfo("CategorÃas")
+ author = getInfo("Grupo")
+ artist = getInfo("Artista")
+
+ description = buildString {
+ additionalInfos.forEach { info ->
+ getInfo(info)?.also {
+ append(info)
+ append(": ")
+ append(it)
+ }
+ }
+ }
+ }
+ }
+
+ private fun Element.getInfo(text: String): String? {
+ return select("div.tag-container:containsOwn($text) a.tag")
+ .joinToString { it.text() }
+ .takeUnless(String::isBlank)
+ }
+
+ // ============================== Chapters ==============================
+ override fun fetchChapterList(manga: SManga): Observable> {
+ val chapter = SChapter.create().apply {
+ url = manga.url.replace("/g/", "/leer/")
+ chapter_number = 1F
+ name = "Chapter"
+ }
+
+ return Observable.just(listOf(chapter))
+ }
+
+ override fun chapterListSelector(): String {
+ throw UnsupportedOperationException()
+ }
+
+ override fun chapterFromElement(element: Element): SChapter {
+ throw UnsupportedOperationException()
+ }
+
+ // =============================== Pages ================================
+ override fun pageListParse(document: Document): List {
+ val script = document.selectFirst("script:containsData(page_image)")!!.data()
+ val pagePaths = script.substringAfter("pages = [")
+ .substringBefore(",]")
+ .substringBefore("]") // Just to make sure
+ .split(',')
+ .map {
+ it.substringAfter(":").substringAfter('"').substringBefore('"')
+ }
+
+ return pagePaths.mapIndexed { index, path ->
+ Page(index, imageUrl = baseUrl + path)
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String {
+ throw UnsupportedOperationException()
+ }
+
+ companion object {
+ const val PREFIX_SEARCH = "id:"
+ }
+}
diff --git a/src/es/hentaimode/src/eu/kanade/tachiyomi/extension/es/hentaimode/HentaiModeUrlActivity.kt b/src/es/hentaimode/src/eu/kanade/tachiyomi/extension/es/hentaimode/HentaiModeUrlActivity.kt
new file mode 100644
index 000000000..f6587b6e3
--- /dev/null
+++ b/src/es/hentaimode/src/eu/kanade/tachiyomi/extension/es/hentaimode/HentaiModeUrlActivity.kt
@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.extension.es.hentaimode
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+/**
+ * Springboard that accepts https://hentaimode.com/g/- intents
+ * and redirects them to the main Tachiyomi process.
+ */
+class HentaiModeUrlActivity : Activity() {
+
+ private val tag = javaClass.simpleName
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+ if (pathSegments != null && pathSegments.size > 1) {
+ val item = pathSegments[1]
+ val mainIntent = Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "${HentaiMode.PREFIX_SEARCH}$item")
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(mainIntent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e(tag, e.toString())
+ }
+ } else {
+ Log.e(tag, "could not parse uri from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}