diff --git a/src/pt/subanimes/AndroidManifest.xml b/src/pt/subanimes/AndroidManifest.xml
new file mode 100644
index 000000000..a5f321261
--- /dev/null
+++ b/src/pt/subanimes/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pt/subanimes/build.gradle b/src/pt/subanimes/build.gradle
new file mode 100644
index 000000000..e89700e43
--- /dev/null
+++ b/src/pt/subanimes/build.gradle
@@ -0,0 +1,14 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'SubAnimes'
+ pkgNameSuffix = 'pt.subanimes'
+ extClass = '.SubAnimes'
+ extVersionCode = 1
+ libVersion = '13'
+}
+
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/pt/subanimes/res/mipmap-hdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..1ff98d629
Binary files /dev/null and b/src/pt/subanimes/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/pt/subanimes/res/mipmap-mdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..71f7b1a2e
Binary files /dev/null and b/src/pt/subanimes/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/pt/subanimes/res/mipmap-xhdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..88745a5ce
Binary files /dev/null and b/src/pt/subanimes/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/pt/subanimes/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4d12d3187
Binary files /dev/null and b/src/pt/subanimes/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/pt/subanimes/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/subanimes/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..b4341dba4
Binary files /dev/null and b/src/pt/subanimes/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBFilters.kt b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBFilters.kt
new file mode 100644
index 000000000..dc299088b
--- /dev/null
+++ b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBFilters.kt
@@ -0,0 +1,177 @@
+package eu.kanade.tachiyomi.animeextension.pt.subanimes
+
+import eu.kanade.tachiyomi.animesource.model.AnimeFilter
+import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
+
+object SBFilters {
+
+ open class QueryPartFilter(
+ displayName: String,
+ val vals: Array>
+ ) : AnimeFilter.Select(
+ displayName,
+ vals.map { it.first }.toTypedArray()
+ ) {
+
+ fun toQueryPart() = vals[state].second
+ }
+
+ private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
+ open class CheckBoxFilterList(name: String, values: List) : AnimeFilter.Group(name, values)
+
+ private inline fun AnimeFilterList.getFirst(): R {
+ return this.filterIsInstance().first()
+ }
+
+ private inline fun AnimeFilterList.asQueryPart(): String {
+ return this.filterIsInstance().joinToString("") {
+ (it as QueryPartFilter).toQueryPart()
+ }
+ }
+
+ class AdultFilter : AnimeFilter.CheckBox("Exibir animes adultos", true)
+
+ class FormatFilter : QueryPartFilter("Tipo de série", SBFiltersData.formats)
+ class StatusFilter : QueryPartFilter("Status do anime", SBFiltersData.status)
+ class TypeFilter : QueryPartFilter("Tipo de áudio", SBFiltersData.types)
+
+ class GenresFilter : CheckBoxFilterList(
+ "Gêneros",
+ SBFiltersData.genres.map { CheckBoxVal(it.first, false) }
+ )
+
+ // Mimicking the order of filters on the source
+ val filterList = AnimeFilterList(
+ TypeFilter(),
+ StatusFilter(),
+ AdultFilter(),
+ FormatFilter(),
+ GenresFilter()
+ )
+
+ data class FilterSearchParams(
+ val adult: Boolean = true,
+ val format: String = "",
+ val genres: List = emptyList(),
+ val status: String = "",
+ val type: String = "",
+ )
+
+ internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
+
+ if (filters.isEmpty()) return FilterSearchParams()
+
+ val genres = filters.getFirst().state
+ .mapNotNull { genre ->
+ if (genre.state) {
+ SBFiltersData.genres.find { it.first == genre.name }!!.second
+ } else { null }
+ }.toList()
+
+ return FilterSearchParams(
+ filters.getFirst().state,
+ filters.asQueryPart(),
+ genres,
+ filters.asQueryPart(),
+ filters.asQueryPart()
+ )
+ }
+
+ private object SBFiltersData {
+ val every = Pair("Qualquer um", "")
+
+ val types = arrayOf(
+ every,
+ Pair("Japonês/Legendado", "1"),
+ Pair("Português/Dublado", "2")
+ )
+
+ val status = arrayOf(
+ every,
+ Pair("Completo", "Completo"),
+ Pair("Em lançamento", "Lançamento")
+ )
+
+ val formats = arrayOf(
+ every,
+ Pair("Anime", "Anime"),
+ Pair("Filme", "Filme")
+ )
+
+ val genres = arrayOf(
+ Pair("Adulto", "334"),
+ Pair("Animação", "2374"),
+ Pair("Arte Marcial", "16"),
+ Pair("Avant Garde", "2846"),
+ Pair("Avant", "2845"),
+ Pair("Aventura", "4"),
+ Pair("Ação", "15"),
+ Pair("Boys Love", "2435"),
+ Pair("Card Battles", "1157"),
+ Pair("Carro", "605"),
+ Pair("China", "865"),
+ Pair("Comédia Romântica", "1254"),
+ Pair("Comédia", "5"),
+ Pair("Corridas", "1514"),
+ Pair("Crime", "1962"),
+ Pair("Culinária", "925"),
+ Pair("Cultivo", "1133"),
+ Pair("Demônio", "19"),
+ Pair("Drama", "36"),
+ Pair("Ecchi", "49"),
+ Pair("Escolar", "140"),
+ Pair("Espacial", "646"),
+ Pair("Esporte", "106"),
+ Pair("Família", "1431"),
+ Pair("Fantasia", "6"),
+ Pair("Ficção Científica", "99"),
+ Pair("Ficção Mítica", "1575"),
+ Pair("Gathering", "2756"),
+ Pair("Gourmet", "2813"),
+ Pair("Harém", "189"),
+ Pair("Histórico", "20"),
+ Pair("Horror", "256"),
+ Pair("Insanidade", "387"),
+ Pair("Isekai", "10"),
+ Pair("Jogos", "63"),
+ Pair("Josei", "733"),
+ Pair("Magia", "82"),
+ Pair("Maid", "2772"),
+ Pair("Mecha", "200"),
+ Pair("Militar", "58"),
+ Pair("Mistério", "50"),
+ Pair("Musical", "112"),
+ Pair("Novel", "951"),
+ Pair("Paródia", "171"),
+ Pair("Policial", "249"),
+ Pair("Psicológico", "66"),
+ Pair("Pós-Apocalíptico", "470"),
+ Pair("Reencarnação", "1134"),
+ Pair("Romance", "7"),
+ Pair("Samurai", "127"),
+ Pair("Sci-fi", "203"),
+ Pair("Seinen", "51"),
+ Pair("Seven", "1449"),
+ Pair("Shoujo Ai", "507"),
+ Pair("Shoujo", "78"),
+ Pair("Shounen Ai", "1326"),
+ Pair("Shounen", "17"),
+ Pair("Slice of Life", "79"),
+ Pair("Sobrenatural", "8"),
+ Pair("Studio Deen", "2451"),
+ Pair("Sunrise", "318"),
+ Pair("Super Poder", "18"),
+ Pair("Suspense", "134"),
+ Pair("Terror", "42"),
+ Pair("Thriller", "960"),
+ Pair("Tragédia", "264"),
+ Pair("Vampiros", "358"),
+ Pair("Vida Diaria", "1518"),
+ Pair("Vida Escolar", "67"),
+ Pair("Violência", "59"),
+ Pair("Yaoi", "1386"),
+ Pair("Yuri", "243"),
+ Pair("Zumbi", "574")
+ )
+ }
+}
diff --git a/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBUrlActivity.kt b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBUrlActivity.kt
new file mode 100644
index 000000000..4d8287680
--- /dev/null
+++ b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SBUrlActivity.kt
@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.animeextension.pt.subanimes
+
+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://subanimes.cc/anime/- intents
+ * and redirects them to the main Aniyomi process.
+ */
+class SBUrlActivity : Activity() {
+
+ private val TAG = "SBUrlActivity"
+
+ 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.ANIMESEARCH"
+ putExtra("query", "${SubAnimes.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/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SubAnimes.kt b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SubAnimes.kt
new file mode 100644
index 000000000..9811858e6
--- /dev/null
+++ b/src/pt/subanimes/src/eu/kanade/tachiyomi/animeextension/pt/subanimes/SubAnimes.kt
@@ -0,0 +1,252 @@
+package eu.kanade.tachiyomi.animeextension.pt.subanimes
+
+import eu.kanade.tachiyomi.animeextension.pt.subanimes.dto.AnimeDataDto
+import eu.kanade.tachiyomi.animeextension.pt.subanimes.dto.SearchResultDto
+import eu.kanade.tachiyomi.animeextension.pt.subanimes.extractors.SubAnimesExtractor
+import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
+import eu.kanade.tachiyomi.animesource.model.AnimesPage
+import eu.kanade.tachiyomi.animesource.model.SAnime
+import eu.kanade.tachiyomi.animesource.model.SEpisode
+import eu.kanade.tachiyomi.animesource.model.Video
+import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.FormBody
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.api.get
+import kotlin.Exception
+
+class SubAnimes : ParsedAnimeHttpSource() {
+
+ override val name = "SubAnimes"
+
+ override val baseUrl = "https://subanimes.cc"
+ private val API_URL = "$baseUrl/wp-admin/admin-ajax.php"
+
+ override val lang = "pt-BR"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.client
+
+ private val json = Json {
+ ignoreUnknownKeys = true
+ }
+
+ // ============================== Popular ===============================
+ override fun popularAnimeSelector() = "div#hype div.aniItem > a"
+ override fun popularAnimeRequest(page: Int) = GET(baseUrl)
+ override fun popularAnimeNextPageSelector() = null // disable it
+ override fun popularAnimeFromElement(element: Element): SAnime =
+ latestUpdatesFromElement(element)
+
+ // ============================== Episodes ==============================
+ override fun episodeListSelector() = "div#episodios div.animeVideosItem > a"
+ private fun episodeListNextPageSelector() = latestUpdatesNextPageSelector()
+
+ override fun fetchEpisodeList(anime: SAnime): Observable
> {
+ return client.newCall(episodeListRequest(anime))
+ .asObservableSuccess()
+ .map { response ->
+ val realDoc = getRealDoc(response.asJsoup())
+ episodeListParse(realDoc).reversed()
+ }
+ }
+
+ override fun episodeListParse(response: Response): List {
+ return episodeListParse(response.asJsoup())
+ }
+
+ private fun episodeListParse(doc: Document): List {
+ val episodeList = mutableListOf()
+ val eps = doc.select(episodeListSelector()).map(::episodeFromElement)
+ episodeList.addAll(eps)
+ val nextPageElement = doc.selectFirst(episodeListNextPageSelector())
+ if (nextPageElement != null) {
+ val nextUrl = nextPageElement.attr("href")
+ val res = client.newCall(GET(nextUrl)).execute()
+ episodeList.addAll(episodeListParse(res))
+ }
+ return episodeList
+ }
+
+ override fun episodeFromElement(element: Element): SEpisode {
+ return SEpisode.create().apply {
+ setUrlWithoutDomain(element.attr("href"))
+ val title = element.attr("title")
+ name = title
+ episode_number = runCatching {
+ title.trim().substringAfterLast(" ").toFloat()
+ }.getOrDefault(0F)
+ }
+ }
+
+ // ============================ Video Links =============================
+ override fun videoListParse(response: Response): List