diff --git a/src/pt/zettahq/AndroidManifest.xml b/src/pt/zettahq/AndroidManifest.xml new file mode 100644 index 000000000..111bc28da --- /dev/null +++ b/src/pt/zettahq/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/zettahq/build.gradle b/src/pt/zettahq/build.gradle new file mode 100644 index 000000000..3adbdd20d --- /dev/null +++ b/src/pt/zettahq/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'ZettaHQ' + extClass = '.ZettaHQ' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/zettahq/res/mipmap-hdpi/ic_launcher.png b/src/pt/zettahq/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..3809c3d33 Binary files /dev/null and b/src/pt/zettahq/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/zettahq/res/mipmap-mdpi/ic_launcher.png b/src/pt/zettahq/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..13c0957d5 Binary files /dev/null and b/src/pt/zettahq/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/zettahq/res/mipmap-xhdpi/ic_launcher.png b/src/pt/zettahq/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..9a703e305 Binary files /dev/null and b/src/pt/zettahq/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/zettahq/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/zettahq/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..dc3a03a92 Binary files /dev/null and b/src/pt/zettahq/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/zettahq/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/zettahq/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..a0214b68a Binary files /dev/null and b/src/pt/zettahq/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/zettahq/src/eu/kanade/tachiyomi/extension/pt/zettahq/ZettaHQ.kt b/src/pt/zettahq/src/eu/kanade/tachiyomi/extension/pt/zettahq/ZettaHQ.kt new file mode 100644 index 000000000..9890f8b47 --- /dev/null +++ b/src/pt/zettahq/src/eu/kanade/tachiyomi/extension/pt/zettahq/ZettaHQ.kt @@ -0,0 +1,259 @@ +package eu.kanade.tachiyomi.extension.pt.zettahq + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Filter +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.ParsedHttpSource +import eu.kanade.tachiyomi.util.asJsoup +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import rx.Observable +import java.text.Normalizer + +class ZettaHQ : ParsedHttpSource() { + + override val name = "ZettaHQ" + + override val baseUrl = "https://zettahq.com" + + override val lang = "pt-BR" + + override val supportsLatest = false + + override val client = network.cloudflareClient + + // ============================== Popular ============================== + override fun popularMangaRequest(page: Int) = GET("$baseUrl/page/$page", headers) + + override fun fetchPopularManga(page: Int): Observable { + if (genreList.isEmpty()) getFilters() + return super.fetchPopularManga(page) + } + + override fun popularMangaSelector() = "div.post-item article" + + override fun popularMangaNextPageSelector() = ".next.page-numbers" + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + element.selectFirst("h3 a")!!.let { anchor -> + title = anchor.text() + setUrlWithoutDomain(anchor.absUrl("href")) + } + thumbnail_url = element.selectFirst("img")?.absUrl("src") + } + + // ============================== Popular ============================== + + override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException() + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException() + override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() + override fun latestUpdatesSelector() = throw UnsupportedOperationException() + + // ============================== Search ============================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/".toHttpUrl().newBuilder() + + var isCategoryEnable = false + var isGenreEnable = false + var isAuthorEnable = false + + filters + .filterNot { it is Filter.Separator } + .sortedByDescending { (it as Sort).priority } + .forEach { filter -> + when (filter) { + is GenreList -> { + val genresSelected = filter.state + .filter { it.state } + .joinToString("+") { it.id } + .takeIf(String::isNotEmpty) ?: return@forEach + + if (isCategoryEnable) { + url.addQueryParameter("tag", genresSelected) + + return@forEach + } + + url.addPathSegment("tag") + .addPathSegment(genresSelected) + + isGenreEnable = isGenreEnable.not() + } + is SelectFilter -> { + val selected = filter.selected() + if (selected.isBlank()) return@forEach + + if (isCategoryEnable || isGenreEnable || isAuthorEnable) { + url.addQueryParameter(filter.query, selected) + return@forEach + } + + url.addPathSegment(filter.query) + .addPathSegment(selected) + + when { + filter.query.equals("autor", true) -> { + isAuthorEnable = isAuthorEnable.not() + } + filter.query.equals("category", true) -> { + isCategoryEnable = isCategoryEnable.not() + } + else -> {} + } + } + else -> {} + } + } + + url.addPathSegment("page") + .addPathSegment(page.toString()) + .addQueryParameter("s", query) + + return GET(url.build(), headers) + } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + if (query.startsWith(PREFIX_SEARCH)) { + val slug = query.substringAfter(PREFIX_SEARCH) + return fetchMangaDetails(SManga.create().apply { url = "/$slug" }) + .map { manga -> MangasPage(listOf(manga), false) } + } + return super.fetchSearchManga(page, query, filters) + } + + override fun searchMangaSelector() = popularMangaSelector() + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + // ============================== Details ============================== + + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + title = document.selectFirst("h1")!!.text() + thumbnail_url = document.selectFirst(".content-container article img:first-child")?.absUrl("src") + genre = document.select(".tags > a.tag").joinToString { it.text() } + author = document.selectFirst("strong:contains(Autor) + a")?.text() + status = SManga.COMPLETED + setUrlWithoutDomain(document.location()) + } + + // ============================== Chapters ============================== + override fun fetchChapterList(manga: SManga): Observable> { + val chapters = listOf( + SChapter.create().apply { + name = "Capítulo Único" + url = manga.url + }, + ) + return Observable.just(chapters) + } + + override fun chapterFromElement(element: Element) = throw UnsupportedOperationException() + override fun chapterListSelector() = throw UnsupportedOperationException() + + // =============================== Pages =============================== + + override fun pageListParse(document: Document): List { + return document.select(".content-container article img").mapIndexed { index, element -> + Page(index, imageUrl = element.absUrl("src")) + } + } + + override fun imageUrlParse(document: Document) = "" + + // =============================== Filters =============================== + + override fun getFilterList(): FilterList { + val filters = mutableListOf>() + if (genreList.isNotEmpty()) { + filters += listOf( + SelectFilter(title = "Categorias", vals = categoryList, query = "category", priority = 3), + Filter.Separator(), + SelectFilter(title = "Personagens", vals = characterList, query = "personagem"), + Filter.Separator(), + SelectFilter(title = "Autor", vals = authorList, query = "autor", priority = 1), + Filter.Separator(), + SelectFilter(title = "Paródia", vals = parodyList, query = "parodia"), + Filter.Separator(), + GenreList(title = "Gêneros", genres = genreList, priority = 2), + ) + } else { + filters += listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros")) + } + return FilterList(filters) + } + + private var categoryList = emptyArray>() + private var authorList = emptyArray>() + private var characterList = emptyArray>() + private var parodyList = emptyArray>() + private var genreList = emptyList() + + private fun getFilters() { + val document = client.newCall(GET("$baseUrl/busca-avancada/", headers)) + .execute() + .asJsoup() + + categoryList = parseOptions(document, "ofcategory") + authorList = parseOptions(document, "ofautor") + characterList = parseOptions(document, "ofpersonagem") + parodyList = parseOptions(document, "ofparodia") + genreList = parseGenres(document) + } + + private fun parseGenres(document: Document): List { + return document.select(".cat-item > label") + .map { label -> + Genre( + name = label.text(), + id = label.text().normalize(), + ) + } + } + + private fun parseOptions(document: Document, attr: String): Array> { + val options = mutableListOf("Todos" to "") + + options += document.select("select[name*=$attr] option").map { option -> + option.text() to option.text().normalize() + } + + return options.toTypedArray() + } + + private fun String.normalize() = this + .lowercase().trim() + .replace(SPACE_REGEX, "-") + .removeAccents() + + private fun String.removeAccents(): String { + val normalized = Normalizer.normalize(this, Normalizer.Form.NFD) + return normalized.replace(Regex("[\\p{InCombiningDiacriticalMarks}]"), "") + } + + interface Sort { + val priority: Int + } + + private class GenreList(title: String, genres: List, override val priority: Int = 0) : + Sort, Filter.Group(title, genres.map { GenreCheckBox(it.name, it.id) }) + + private class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name) + private class Genre(val name: String, val id: String = name) + + private open class SelectFilter(title: String, private val vals: Array>, state: Int = 0, val query: String = "", override val priority: Int = 0) : + Sort, Filter.Select(title, vals.map { it.first }.toTypedArray(), state) { + fun selected() = vals[state].second + } + + companion object { + const val PREFIX_SEARCH = "id:" + val SPACE_REGEX = """\s+""".toRegex() + } +} diff --git a/src/pt/zettahq/src/eu/kanade/tachiyomi/extension/pt/zettahq/ZettaHQUrlActivity.kt b/src/pt/zettahq/src/eu/kanade/tachiyomi/extension/pt/zettahq/ZettaHQUrlActivity.kt new file mode 100644 index 000000000..138407ca1 --- /dev/null +++ b/src/pt/zettahq/src/eu/kanade/tachiyomi/extension/pt/zettahq/ZettaHQUrlActivity.kt @@ -0,0 +1,35 @@ +package eu.kanade.tachiyomi.extension.pt.zettahq + +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 ZettaHQUrlActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && pathSegments.size >= 1) { + val item = pathSegments[pathSegments.size - 1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${ZettaHQ.PREFIX_SEARCH}$item") + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("ZettaHQUrlActivity", e.toString()) + } + } else { + Log.e("ZettaHQUrlActivity", "could not parse uri from intent $intent") + } + + finish() + exitProcess(0) + } +}