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