Add ZettaHQ (#5844)

* Add ZettaHQ

* Remove CategoryFilter class
This commit is contained in:
Chopper 2024-11-03 08:54:30 -03:00 committed by GitHub
parent 9f4a6139cd
commit 5dfa53e98f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 324 additions and 0 deletions

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".pt.zettahq.ZettaHQUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="zettahq.com"
android:pathPattern="/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,8 @@
ext {
extName = 'ZettaHQ'
extClass = '.ZettaHQ'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -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<MangasPage> {
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<MangasPage> {
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<List<SChapter>> {
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<Page> {
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<Filter<*>>()
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<Pair<String, String>>()
private var authorList = emptyArray<Pair<String, String>>()
private var characterList = emptyArray<Pair<String, String>>()
private var parodyList = emptyArray<Pair<String, String>>()
private var genreList = emptyList<Genre>()
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<Genre> {
return document.select(".cat-item > label")
.map { label ->
Genre(
name = label.text(),
id = label.text().normalize(),
)
}
}
private fun parseOptions(document: Document, attr: String): Array<Pair<String, String>> {
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<Genre>, override val priority: Int = 0) :
Sort, Filter.Group<GenreCheckBox>(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<Pair<String, String>>, state: Int = 0, val query: String = "", override val priority: Int = 0) :
Sort, Filter.Select<String>(title, vals.map { it.first }.toTypedArray(), state) {
fun selected() = vals[state].second
}
companion object {
const val PREFIX_SEARCH = "id:"
val SPACE_REGEX = """\s+""".toRegex()
}
}

View File

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