diff --git a/src/pt/blackscans/AndroidManifest.xml b/src/pt/blackscans/AndroidManifest.xml
new file mode 100644
index 000000000..8c685c35c
--- /dev/null
+++ b/src/pt/blackscans/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pt/blackscans/build.gradle b/src/pt/blackscans/build.gradle
new file mode 100644
index 000000000..caf40184e
--- /dev/null
+++ b/src/pt/blackscans/build.gradle
@@ -0,0 +1,7 @@
+ext {
+ extName = 'Black Scans'
+ extClass = '.BlackScans'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/blackscans/res/mipmap-hdpi/ic_launcher.png b/src/pt/blackscans/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..dbbb59182
Binary files /dev/null and b/src/pt/blackscans/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/blackscans/res/mipmap-mdpi/ic_launcher.png b/src/pt/blackscans/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..ef73bba4b
Binary files /dev/null and b/src/pt/blackscans/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/blackscans/res/mipmap-xhdpi/ic_launcher.png b/src/pt/blackscans/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..03373237c
Binary files /dev/null and b/src/pt/blackscans/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/blackscans/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/blackscans/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..41df90443
Binary files /dev/null and b/src/pt/blackscans/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/blackscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/blackscans/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8e355c5c3
Binary files /dev/null and b/src/pt/blackscans/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/BlackScans.kt b/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/BlackScans.kt
new file mode 100644
index 000000000..d908b87af
--- /dev/null
+++ b/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/BlackScans.kt
@@ -0,0 +1,179 @@
+package eu.kanade.tachiyomi.extension.pt.blackscans
+
+import android.annotation.SuppressLint
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+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.HttpSource
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.decodeFromStream
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import okio.Buffer
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+
+class BlackScans : HttpSource() {
+
+ override val name = "Black Scans"
+
+ override val baseUrl = "https://blackscans.site"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ override val client = network.cloudflareClient.newBuilder()
+ .rateLimitHost(API_URL.toHttpUrl(), 2)
+ .build()
+
+ private val json: Json by injectLazy()
+
+ // ============================== Popular ==============================
+
+ override fun popularMangaRequest(page: Int) = GET("$API_URL/api/series/", headers)
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val mangas = response.parseAs>().map { manga ->
+ SManga.create().apply {
+ title = manga.title
+ thumbnail_url = "$API_URL/media/${manga.cover}"
+ url = "/series/${manga.code}"
+ }
+ }
+ return MangasPage(mangas, false)
+ }
+
+ // ============================== Latest ==============================
+
+ override fun latestUpdatesRequest(page: Int) = GET("$API_URL/api/series/updates/", headers)
+
+ override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
+
+ // ============================== Search ==============================
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = popularMangaRequest(page)
+
+ override fun searchMangaParse(response: Response) = popularMangaParse(response)
+
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
+ if (query.startsWith(PREFIX_SEARCH)) {
+ val mangaCode = query.substringAfter(PREFIX_SEARCH)
+ return fetchMangaDetails(SManga.create().apply { url = "/series/$mangaCode" })
+ .map { manga -> MangasPage(listOf(manga), false) }
+ }
+
+ return super.fetchSearchManga(page, query, filters).map { mangasPage ->
+ val mangas = mangasPage.mangas.filter { manga -> manga.title.contains(query, true) }
+ mangasPage.copy(mangas)
+ }
+ }
+
+ // ============================== Details =============================
+
+ override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}"
+
+ override fun mangaDetailsRequest(manga: SManga) =
+ POST("$API_URL/api/serie/", headers, manga.createPostPayload())
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ return response.parseAs().let { dto ->
+ SManga.create().apply {
+ title = dto.title
+ description = dto.synopsis
+ thumbnail_url = "$API_URL/media/${dto.cover}"
+ author = dto.author
+ artist = dto.artist
+ genre = dto.genres.joinToString()
+ url = "/series/${dto.code}"
+ status = dto.status.toMangaStatus()
+ }
+ }
+ }
+
+ private fun String.toMangaStatus(): Int {
+ return when (this.lowercase()) {
+ "ongoing" -> SManga.ONGOING
+ "completed" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ }
+
+ // ============================== Chapters ============================
+
+ override fun getChapterUrl(chapter: SChapter) = "$baseUrl${chapter.url}"
+
+ override fun chapterListRequest(manga: SManga): Request {
+ val payload = manga.createPostPayload("series_code")
+ return POST("$API_URL/api/series/chapters/", headers, payload)
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val series = response.request.body!!.parseAs()
+
+ return response.parseAs().chapters.map { chapter ->
+ SChapter.create().apply {
+ name = chapter.name
+ date_upload = chapter.uploadAt.toDate()
+ url = "/series/${series.code}/${chapter.code}"
+ }
+ }
+ }
+
+ // ============================== Pages ===============================
+
+ override fun imageUrlParse(response: Response) = ""
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ val chapterCode = chapter.url.substringAfterLast("/")
+ val payload = """{"chapter_code":"$chapterCode"}"""
+ .toRequestBody("application/json".toMediaType())
+ return POST("$API_URL/api/chapter/info/", headers, payload)
+ }
+
+ override fun pageListParse(response: Response): List {
+ return response.parseAs().images.mapIndexed { index, imageUrl ->
+ Page(index, imageUrl = "$API_URL//media/$imageUrl")
+ }
+ }
+
+ // ============================== Utils ===============================
+
+ @Serializable
+ private class SeriesDto(@SerialName("series_code") val code: String)
+
+ private fun SManga.createPostPayload(field: String = "code"): RequestBody {
+ val mangaCode = url.substringAfterLast("/")
+ return """{"$field": "$mangaCode"}""".toRequestBody("application/json".toMediaType())
+ }
+
+ private inline fun Response.parseAs(): T = use {
+ json.decodeFromStream(it.body.byteStream())
+ }
+ private inline fun RequestBody.parseAs(): T =
+ json.decodeFromString(Buffer().also { writeTo(it) }.readUtf8())
+
+ private fun String.toDate() =
+ try { dateFormat.parse(this)!!.time } catch (_: Exception) { 0 }
+
+ companion object {
+ const val API_URL = "https://api.blackscans.site"
+ const val PREFIX_SEARCH = "id:"
+
+ @SuppressLint("SimpleDateFormat")
+ val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")
+ }
+}
diff --git a/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/BlackScansUrlActivity.kt b/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/BlackScansUrlActivity.kt
new file mode 100644
index 000000000..3f2b02e16
--- /dev/null
+++ b/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/BlackScansUrlActivity.kt
@@ -0,0 +1,37 @@
+package eu.kanade.tachiyomi.extension.pt.blackscans
+
+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 BlackScansUrlActivity : 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", "${BlackScans.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)
+ }
+}
diff --git a/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/MangaDetailsDto.kt b/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/MangaDetailsDto.kt
new file mode 100644
index 000000000..14b6cb167
--- /dev/null
+++ b/src/pt/blackscans/src/eu/kanade/tachiyomi/extension/pt/blackscans/MangaDetailsDto.kt
@@ -0,0 +1,44 @@
+package eu.kanade.tachiyomi.extension.pt.blackscans
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.json.JsonNames
+
+@Serializable
+class MangaDetailsDto(
+ val title: String,
+ val artist: String,
+ val author: String,
+ val code: String,
+ val genres: List,
+ @SerialName("path_cover")
+ val cover: String,
+ val status: String,
+ val synopsis: String,
+)
+
+@Serializable
+class MangaDto(
+ val code: String,
+ val title: String,
+ @JsonNames("path_cover")
+ val cover: String,
+)
+
+@Serializable
+class ChapterList(
+ val chapters: List,
+)
+
+@Serializable
+data class Chapter(
+ val code: String,
+ val name: String,
+ @SerialName("upload_date")
+ val uploadAt: String,
+)
+
+@Serializable
+class PagesDto(
+ val images: List,
+)