diff --git a/src/pt/blackoutcomics/AndroidManifest.xml b/src/pt/blackoutcomics/AndroidManifest.xml new file mode 100644 index 000000000..5a029e932 --- /dev/null +++ b/src/pt/blackoutcomics/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/blackoutcomics/build.gradle b/src/pt/blackoutcomics/build.gradle new file mode 100644 index 000000000..4f150d6ae --- /dev/null +++ b/src/pt/blackoutcomics/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Blackout Comics' + extClass = '.BlackoutComics' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/blackoutcomics/res/mipmap-hdpi/ic_launcher.png b/src/pt/blackoutcomics/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..cff87ebd7 Binary files /dev/null and b/src/pt/blackoutcomics/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/blackoutcomics/res/mipmap-mdpi/ic_launcher.png b/src/pt/blackoutcomics/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..408ca9e72 Binary files /dev/null and b/src/pt/blackoutcomics/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/blackoutcomics/res/mipmap-xhdpi/ic_launcher.png b/src/pt/blackoutcomics/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..3dc19c18f Binary files /dev/null and b/src/pt/blackoutcomics/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/blackoutcomics/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/blackoutcomics/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..1aff8f986 Binary files /dev/null and b/src/pt/blackoutcomics/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/blackoutcomics/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/blackoutcomics/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5ffb64915 Binary files /dev/null and b/src/pt/blackoutcomics/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/blackoutcomics/src/eu/kanade/tachiyomi/extension/pt/blackoutcomics/BlackoutComics.kt b/src/pt/blackoutcomics/src/eu/kanade/tachiyomi/extension/pt/blackoutcomics/BlackoutComics.kt new file mode 100644 index 000000000..3a7ead6ad --- /dev/null +++ b/src/pt/blackoutcomics/src/eu/kanade/tachiyomi/extension/pt/blackoutcomics/BlackoutComics.kt @@ -0,0 +1,178 @@ +package eu.kanade.tachiyomi.extension.pt.blackoutcomics + +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.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 +import java.text.SimpleDateFormat +import java.util.Locale + +class BlackoutComics : ParsedHttpSource() { + + override val name = "Blackout Comics" + + override val baseUrl = "https://blackoutcomics.com" + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client by lazy { + network.client.newBuilder() + .rateLimitHost(baseUrl.toHttpUrl(), 2) + .build() + } + + override fun headersBuilder() = + super.headersBuilder() + .add("Referer", "$baseUrl/") + .add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8") + .add("Accept-Language", "en-US,en;q=0.5") + + // ============================== Popular =============================== + override fun popularMangaRequest(page: Int) = GET("$baseUrl/ranking") + + override fun popularMangaSelector() = "section > div.container div > a" + + override fun popularMangaFromElement(element: Element) = SManga.create().apply { + setUrlWithoutDomain(element.attr("href")) + thumbnail_url = element.selectFirst("img")?.absUrl("src") + title = element.selectFirst("p, span.text-comic")?.text() ?: "Manga" + } + + override fun popularMangaNextPageSelector() = null + + // =============================== Latest =============================== + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/recentes") + + override fun latestUpdatesSelector() = popularMangaSelector() + + override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) + + override fun latestUpdatesNextPageSelector() = null + + // =============================== 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/comics/$id")) + .asObservableSuccess() + .map(::searchMangaByIdParse) + } else { + super.fetchSearchManga(page, query, filters) + } + } + + private fun searchMangaByIdParse(response: Response): MangasPage { + val details = mangaDetailsParse(response.use { it.asJsoup() }) + return MangasPage(listOf(details), false) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + // Using URLBuilder just to prevent issues with strange queries + val url = "$baseUrl/comics".toHttpUrl().newBuilder() + .addQueryParameter("search", query) + .build() + return GET(url, headers) + } + + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = null + + // =========================== Manga Details ============================ + override fun mangaDetailsParse(document: Document) = SManga.create().apply { + val row = document.selectFirst("section > div.container > div.row")!! + thumbnail_url = row.selectFirst("img")?.absUrl("src") + title = row.selectFirst("div.trailer-content > h2")?.text() ?: "Manga" + + with(row.selectFirst("div.trailer-content:has(h3:containsOwn(Detalhes))")!!) { + println(outerHtml()) + artist = getInfo("Artista") + author = getInfo("Autor") + genre = getInfo("Genêros") + status = when (getInfo("Status")) { + "Completo" -> SManga.COMPLETED + "Em Lançamento" -> SManga.ONGOING + else -> SManga.UNKNOWN + } + + description = buildString { + // Synopsis + row.selectFirst("h3:containsOwn(Descrição) + p")?.ownText()?.also { + append("$it\n\n") + } + + row.selectFirst("h2:contains($title) + p")?.ownText()?.also { + // Alternative title + append("Título alternativo: $it\n") + } + + // Additional info + listOf("Editora", "Lançamento", "Scans", "Tradução", "Cleaner", "Vizualizações") + .forEach { item -> + selectFirst("p:contains($item)") + ?.text() + ?.also { append("$it\n") } + } + } + } + } + + private fun Element.getInfo(text: String) = + selectFirst("p:contains($text)")?.run { + selectFirst("b")?.text() ?: ownText() + } + + // ============================== Chapters ============================== + override fun chapterListSelector() = "section.relese > div.container > div.row h5:has(a)" + + override fun chapterFromElement(element: Element) = SChapter.create().apply { + element.selectFirst("form + a")!!.run { + setUrlWithoutDomain(attr("href")) + name = text() + chapter_number = name.substringAfter(" ").toFloatOrNull() ?: 1F + } + + date_upload = element.selectFirst("form + a + span")?.text().orEmpty().toDate() + } + + // =============================== Pages ================================ + override fun pageListParse(document: Document): List { + return document.select("div.chapter-image canvas").mapIndexed { index, item -> + Page(index, "", item.absUrl("data-src")) + } + } + + override fun imageUrlParse(document: Document): String { + throw UnsupportedOperationException() + } + + // ============================= Utilities ============================== + private fun String.toDate(): Long { + return runCatching { DATE_FORMATTER.parse(trim())?.time } + .getOrNull() ?: 0L + } + + companion object { + const val PREFIX_SEARCH = "id:" + + private val DATE_FORMATTER by lazy { + SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH) + } + } +} diff --git a/src/pt/blackoutcomics/src/eu/kanade/tachiyomi/extension/pt/blackoutcomics/BlackoutComicsUrlActivity.kt b/src/pt/blackoutcomics/src/eu/kanade/tachiyomi/extension/pt/blackoutcomics/BlackoutComicsUrlActivity.kt new file mode 100644 index 000000000..282f47709 --- /dev/null +++ b/src/pt/blackoutcomics/src/eu/kanade/tachiyomi/extension/pt/blackoutcomics/BlackoutComicsUrlActivity.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.extension.pt.blackoutcomics + +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://blackoutcomics.com/comics/ intents + * and redirects them to the main Tachiyomi process. + */ +class BlackoutComicsUrlActivity : 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", "${BlackoutComics.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) + } +}