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) + } +}