From 31e2508898244da698d2a2bafd700f3d6c528d63 Mon Sep 17 00:00:00 2001 From: iq8 <266871934+disk-iq8@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:53:38 +0300 Subject: [PATCH 1/2] 1.6 --- src/pt/spectralscan/build.gradle.kts | 10 +- .../extension/pt/spectralscan/Filters.kt | 62 ++++ .../extension/pt/spectralscan/NexusDecrypt.kt | 2 +- .../extension/pt/spectralscan/NexusToons.kt | 301 +++++++++--------- .../pt/spectralscan/NexusToonsDto.kt | 275 +--------------- 5 files changed, 224 insertions(+), 426 deletions(-) create mode 100644 src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/Filters.kt diff --git a/src/pt/spectralscan/build.gradle.kts b/src/pt/spectralscan/build.gradle.kts index 6a4d7a74d77..974a6485b76 100644 --- a/src/pt/spectralscan/build.gradle.kts +++ b/src/pt/spectralscan/build.gradle.kts @@ -6,13 +6,17 @@ plugins { keiyoushi { name = "Nexus Toons" - versionCode = 62 - contentWarning = ContentWarning.NSFW // or MIXED, please confirm - libVersion = "1.4" + versionCode = 0 + contentWarning = ContentWarning.MIXED + libVersion = "1.6" source { lang = "pt-BR" baseUrl = "https://nx-toons.xyz" id = 5304928452449566995L } + + deeplink { + path("/manga/..*") + } } diff --git a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/Filters.kt b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/Filters.kt new file mode 100644 index 00000000000..3cbb7442895 --- /dev/null +++ b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/Filters.kt @@ -0,0 +1,62 @@ +package eu.kanade.tachiyomi.extension.pt.spectralscan + +import eu.kanade.tachiyomi.source.model.Filter + +class SelectFilter( + displayName: String, + val parameter: String, + private val vals: Array>, + state: Int = 0, +) : Filter.Select(displayName, vals.map { it.first }.toTypedArray(), state) { + fun selected() = vals[state].second +} + +class CheckboxGroup( + displayName: String, + val parameter: String, + val items: List, +) : Filter.Group(displayName, items) { + fun selected() = items.filter { it.state }.map { it.value } +} + +class CheckboxItem( + displayName: String, + val value: String, +) : Filter.CheckBox(displayName, false) + +val sortList = arrayOf( + "Visualizações" to "views", + "Atualização" to "updatedAt", + "Adicionado" to "created", + "Título" to "title", + "Avaliações" to "rating", + "Capitulos" to "chapters", + "Ano" to "releaseYear", +) + +val orderList = arrayOf( + "Decrescente" to "desc", + "Crescente" to "asc", +) + +val statusList = arrayOf( + Pair("Em Andamento", "ongoing"), + Pair("Completo", "completed"), + Pair("Cancelado", "cancelled"), + Pair("Hiato", "hiatus"), +) + +val typeList = arrayOf( + Pair("Manga", "manga"), + Pair("Manhwa", "manhwa"), + Pair("Manhua", "manhua"), + Pair("Webtoon", "webtoon"), + Pair("Comic", "comic"), + Pair("HQ", "hq"), + Pair("Pornhwa", "pornhwa"), +) + +val categoryModeList = arrayOf( + "Qualquer categoria (OU)" to "or", + "Todas categorias (E)" to "and", +) diff --git a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusDecrypt.kt b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusDecrypt.kt index 3797f564088..320fb92f63b 100644 --- a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusDecrypt.kt +++ b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusDecrypt.kt @@ -133,7 +133,7 @@ object NexusDecrypt { return@Interceptor response } - val body = response.body?.string() ?: return@Interceptor response + val body = response.body.string() val decryptedBody = if (isEncryptedResponse(body)) { try { diff --git a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt index 7689f2d0eef..eb9af67472c 100644 --- a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt +++ b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt @@ -1,138 +1,74 @@ package eu.kanade.tachiyomi.extension.pt.spectralscan - -import android.content.SharedPreferences import android.util.Base64 import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat -import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.ConfigurableSource 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 eu.kanade.tachiyomi.source.model.SMangaUpdate import keiyoushi.annotation.Source +import keiyoushi.network.get import keiyoushi.network.rateLimit -import keiyoushi.utils.getPreferencesLazy +import keiyoushi.source.KeiSource +import keiyoushi.utils.getPreferences import keiyoushi.utils.parseAs +import keiyoushi.utils.toJsonElement +import kotlinx.serialization.json.JsonElement +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request +import okhttp3.OkHttpClient import okhttp3.Response import kotlin.random.Random import kotlin.time.Duration.Companion.seconds @Source abstract class NexusToons : - HttpSource(), + KeiSource(), ConfigurableSource { - override val supportsLatest = true - - private val preferences: SharedPreferences by getPreferencesLazy() - - override val client = network.client.newBuilder() - .addInterceptor(NexusDecrypt.createInterceptor()) - .rateLimit(3, 1.seconds) - .build() - - private val apiHeaders by lazy { - headers.newBuilder() - .add("Accept", "application/json") - .add("Referer", "$baseUrl/") - .build() - } - - private val onlyNsfw: Boolean - get() = preferences.getBoolean(PREF_ONLY_NSFW_KEY, PREF_ONLY_NSFW_DEFAULT) - - // ==================== URL Helpers ========================== - - override fun getMangaUrl(manga: SManga) = "$baseUrl${manga.url}" - - override fun getChapterUrl(chapter: SChapter): String { - val parts = chapter.url.substringAfter("/read/").split("/") - val chapterId = parts[0] - val mangaSlug = parts.getOrNull(1) ?: "" - return "$baseUrl/r/${encodeChapterUrl(chapterId, mangaSlug)}" - } - - private fun encodeChapterUrl(chapterId: String, mangaSlug: String = ""): String { - val timestamp = System.currentTimeMillis().toString(36) - val padding = randomString(20 + Random.nextInt(11)) - val data = "$chapterId|$mangaSlug|$timestamp|$padding" - - val xored = xorCipher(data, CHAPTER_ENCRYPTION_KEY) - val firstEncode = base64UrlEncode(xored) - val secondEncode = base64UrlEncode("$firstEncode|${randomString(10)}") - - return if (secondEncode.length >= 64) { - secondEncode - } else { - secondEncode + randomString(64 - secondEncode.length) - } + override fun OkHttpClient.Builder.configureClient() = apply { + addInterceptor(NexusDecrypt.createInterceptor()) + rateLimit(3, 1.seconds) } - private fun xorCipher(input: String, key: String): String = input.mapIndexed { i, char -> - (char.code xor key[i % key.length].code).toChar() - }.joinToString("") - - private fun base64UrlEncode(input: String): String { - val bytes = input.map { it.code.toByte() }.toByteArray() - return Base64.encodeToString(bytes, Base64.NO_WRAP) - .replace('+', '-') - .replace('/', '_') - .trimEnd('=') - } - - private fun randomString(length: Int): String { - val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - return buildString(length) { - repeat(length) { append(chars.random()) } - } - } + private val apiHeaders get() = + headersBuilder() + .set("Accept", "application/json") + .build() // ==================== Popular ========================== - override fun popularMangaRequest(page: Int): Request { - val url = "$baseUrl/api/mangas".toHttpUrl().newBuilder() - .addQueryParameter("page", page.toString()) - .addQueryParameter("limit", "50") - .addQueryParameter("includeNsfw", "true") - .apply { if (onlyNsfw) addQueryParameter("onlyNsfw", "true") } - .addQueryParameter("sortBy", "views") - .build() - return GET(url, apiHeaders) - } - - override fun popularMangaParse(response: Response): MangasPage { - val result = response.parseAs() - val mangas = result.data.orEmpty().map { it.toSManga() } - val hasNextPage = result.page < result.pages - return MangasPage(mangas, hasNextPage) - } + override suspend fun getPopularManga(page: Int) = getSearchMangaList( + page, + "", + FilterList( + SelectFilter("", "sortBy", sortList), + ), + ) // ==================== Latest ========================== - override fun latestUpdatesRequest(page: Int): Request { - val url = "$baseUrl/api/mangas".toHttpUrl().newBuilder() - .addQueryParameter("page", page.toString()) - .addQueryParameter("limit", "50") - .addQueryParameter("includeNsfw", "true") - .apply { if (onlyNsfw) addQueryParameter("onlyNsfw", "true") } - .addQueryParameter("sortBy", "lastChapterAt") - .build() - return GET(url, apiHeaders) - } - - override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + override suspend fun getLatestUpdates(page: Int) = getSearchMangaList(page, "", FilterList()) // ==================== Search ========================== - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + override suspend fun getMangaByUrl(url: HttpUrl): SManga? { + if (url.pathSegments.count(String::isNotBlank) < 2) return null + return fetchMangaUpdate( + SManga.create().apply { this.url = url.encodedPath }, + emptyList(), + true, + false, + ).manga + } + + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { val url = "$baseUrl/api/mangas".toHttpUrl().newBuilder() .addQueryParameter("page", page.toString()) - .addQueryParameter("limit", "30") + .addQueryParameter("limit", "50") .addQueryParameter("includeNsfw", "true") if (query.isNotBlank()) { @@ -179,6 +115,7 @@ abstract class NexusToons : url.addQueryParameter("sortBy", sortBy) url.addQueryParameter("sortOrder", sortOrder) url.addQueryParameter("categoryMode", categoryMode) + if (onlyNsfw) url.addQueryParameter("onlyNsfw", "true") if (statusList.isNotEmpty()) { url.addQueryParameter("status", statusList.joinToString(",")) @@ -193,42 +130,47 @@ abstract class NexusToons : url.addQueryParameter("themes", themeList.joinToString(",")) } - return GET(url.build(), apiHeaders) + return parseSearch( + client.get(url.build(), apiHeaders), + ) } - override fun searchMangaParse(response: Response) = popularMangaParse(response) - - // ==================== Details ======================= - - private fun getMangaSlug(url: String) = url.substringAfter("/manga/").trimEnd('/') - - override fun mangaDetailsRequest(manga: SManga): Request { - val slug = getMangaSlug(manga.url) - return GET("$baseUrl/api/manga/$slug", apiHeaders) + private fun parseSearch(response: Response): MangasPage { + val result = response.parseAs() + val mangas = result.data.orEmpty().map { it.toSManga() } + val hasNextPage = result.page < result.pages + return MangasPage(mangas, hasNextPage) } - override fun mangaDetailsParse(response: Response): SManga = response.parseAs().toSManga() + // ==================== Details ======================= - // ==================== Chapter ======================= + override val supportRelatedMangasBySearch = true - override fun chapterListRequest(manga: SManga): Request { + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { val slug = getMangaSlug(manga.url) - return GET("$baseUrl/api/manga/$slug", apiHeaders) - } + val response = client.get("$baseUrl/api/manga/$slug", apiHeaders) + val mangaDto = response.parseAs() + val chapterList = mangaDto.chapters.orEmpty().map { + it.toSChapter(mangaDto.slug) + } - override fun chapterListParse(response: Response): List { - val manga = response.parseAs() - return manga.chapters.orEmpty().map { it.toSChapter(manga.slug) } + return SMangaUpdate( + mangaDto.toSManga(), + chapterList, + ) } // ==================== Page ========================== - override fun pageListRequest(chapter: SChapter): Request { + override suspend fun getPageList(chapter: SChapter): List { val chapterId = chapter.url.substringAfter("/read/").substringBefore("/") - return GET("$baseUrl/api/read/$chapterId", apiHeaders) - } + val response = client.get("$baseUrl/api/read/$chapterId", apiHeaders) - override fun pageListParse(response: Response): List { val readResponse = response.parseAs() val pages = readResponse.pages if (pages.first().imageUrl != null) { @@ -247,44 +189,45 @@ abstract class NexusToons : } } - override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() - - override fun imageRequest(page: Page): Request { - val headers = headersBuilder() - .set("Referer", "$baseUrl/") - .build() - return GET(page.imageUrl!!, headers) - } - // ==================== Filters ========================== - override fun getFilterList() = FilterList( - SelectFilter("Ordenar Por", "sortBy", sortList), - SelectFilter("Ordem", "sortOrder", orderList), - SelectFilter("Modo de Categoria", "categoryMode", categoryModeList), - CheckboxGroup( - "Status", - "status", - statusList.map { CheckboxItem(it.first, it.second) }, - ), - CheckboxGroup( - "Tipo", - "type", - typeList.map { CheckboxItem(it.first, it.second) }, - ), - CheckboxGroup( - "Gêneros", - "genres", - genreList.map { CheckboxItem(it.first, it.second) }, - ), - CheckboxGroup( - "Temas", - "themes", - themeList.map { CheckboxItem(it.first, it.second) }, - ), + override val supportsFilterFetching = true + + override suspend fun fetchFilterData() = client.get("$baseUrl/api/categories").parseAs>().toJsonElement() + + override fun getFilterList(data: JsonElement?) = FilterList( + listOf( + SelectFilter("Ordenar Por", "sortBy", sortList), + SelectFilter("Ordem", "sortOrder", orderList), + SelectFilter("Modo de Categoria", "categoryMode", categoryModeList), + CheckboxGroup( + "Status", + "status", + statusList.map { CheckboxItem(it.first, it.second) }, + ), + CheckboxGroup( + "Tipo", + "type", + typeList.map { CheckboxItem(it.first, it.second) }, + ), + ) + data?.parseAs>()?.let { filters -> + listOfNotNull( + filters.filter { it.type == "genre" }.takeIf { it.isNotEmpty() }?.let { genres -> + CheckboxGroup("Gêneros", "genres", genres.map { CheckboxItem(it.name, it.id.toString()) }) + }, + filters.filter { it.type == "theme" }.takeIf { it.isNotEmpty() }?.let { themes -> + CheckboxGroup("Temas", "themes", themes.map { CheckboxItem(it.name, it.id.toString()) }) + }, + ) + }.orEmpty(), ) - // ==================== Settings ========================== + // ================== Preferences ======================== + + private val preferences = getPreferences() + + private val onlyNsfw: Boolean + get() = preferences.getBoolean(PREF_ONLY_NSFW_KEY, PREF_ONLY_NSFW_DEFAULT) override fun setupPreferenceScreen(screen: PreferenceScreen) { SwitchPreferenceCompat(screen.context).apply { @@ -295,6 +238,52 @@ abstract class NexusToons : }.also(screen::addPreference) } + // ==================== URL Helpers ========================== + + private fun getMangaSlug(url: String) = url.substringAfter("/manga/").trimEnd('/') + + override fun getChapterUrl(chapter: SChapter): String { + val parts = chapter.url.substringAfter("/read/").split("/") + val chapterId = parts[0] + val mangaSlug = parts.getOrNull(1) ?: "" + return "$baseUrl/r/${encodeChapterUrl(chapterId, mangaSlug)}" + } + + private fun encodeChapterUrl(chapterId: String, mangaSlug: String = ""): String { + val timestamp = System.currentTimeMillis().toString(36) + val padding = randomString(20 + Random.nextInt(11)) + val data = "$chapterId|$mangaSlug|$timestamp|$padding" + + val xored = xorCipher(data, CHAPTER_ENCRYPTION_KEY) + val firstEncode = base64UrlEncode(xored) + val secondEncode = base64UrlEncode("$firstEncode|${randomString(10)}") + + return if (secondEncode.length >= 64) { + secondEncode + } else { + secondEncode + randomString(64 - secondEncode.length) + } + } + + private fun xorCipher(input: String, key: String): String = input.mapIndexed { i, char -> + (char.code xor key[i % key.length].code).toChar() + }.joinToString("") + + private fun base64UrlEncode(input: String): String { + val bytes = input.map { it.code.toByte() }.toByteArray() + return Base64.encodeToString(bytes, Base64.NO_WRAP) + .replace('+', '-') + .replace('/', '_') + .trimEnd('=') + } + + private fun randomString(length: Int): String { + val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + return buildString(length) { + repeat(length) { append(chars.random()) } + } + } + companion object { private const val PREF_ONLY_NSFW_KEY = "pref_only_nsfw" private const val PREF_ONLY_NSFW_DEFAULT = false diff --git a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToonsDto.kt b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToonsDto.kt index b0445ccd070..ae26aad2421 100644 --- a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToonsDto.kt +++ b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToonsDto.kt @@ -1,19 +1,10 @@ package eu.kanade.tachiyomi.extension.pt.spectralscan -import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import keiyoushi.utils.tryParse import kotlinx.serialization.Serializable -import java.text.SimpleDateFormat -import java.util.Locale -import java.util.TimeZone - -private val dateFormat by lazy { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT).apply { - timeZone = TimeZone.getTimeZone("UTC") - } -} +import kotlin.time.Instant // ==================== API Response DTOs ==================== @@ -76,6 +67,13 @@ class EncryptedResponse( val v: Int, ) +@Serializable +class GenreDto( + val name: String, + val id: Int, + val type: String, +) + // ==================== Conversion Functions ==================== fun MangaListDto.toSManga() = SManga.create().apply { @@ -102,7 +100,7 @@ fun ChapterDto.toSChapter(mangaSlug: String) = SChapter.create().apply { } else { "Capítulo ${number.removeSuffix(".0")}" } - date_upload = dateFormat.tryParse(createdAt.substringBefore(".")) + date_upload = Instant.tryParse(createdAt) } private fun String?.parseStatus() = when (this?.lowercase()) { @@ -112,258 +110,3 @@ private fun String?.parseStatus() = when (this?.lowercase()) { "cancelled" -> SManga.CANCELLED else -> SManga.UNKNOWN } - -// ==================== Filters ==================== - -class SelectFilter( - displayName: String, - val parameter: String, - private val vals: Array>, - state: Int = 0, -) : Filter.Select(displayName, vals.map { it.first }.toTypedArray(), state) { - fun selected() = vals[state].second -} - -class CheckboxGroup( - displayName: String, - val parameter: String, - val items: List, -) : Filter.Group(displayName, items) { - fun selected() = items.filter { it.state }.map { it.value } -} - -class CheckboxItem( - displayName: String, - val value: String, -) : Filter.CheckBox(displayName, false) - -val sortList = arrayOf( - "Visualizações" to "views", - "Atualização" to "updatedAt", - "Adicionado" to "created", - "Título" to "title", - "Avaliações" to "rating", - "Capitulos" to "chapters", - "Ano" to "releaseYear", -) - -val orderList = arrayOf( - "Decrescente" to "desc", - "Crescente" to "asc", -) - -val statusList = arrayOf( - Pair("Em Andamento", "ongoing"), - Pair("Completo", "completed"), - Pair("Cancelado", "cancelled"), - Pair("Hiato", "hiatus"), -) - -val typeList = arrayOf( - Pair("Manga", "manga"), - Pair("Manhwa", "manhwa"), - Pair("Manhua", "manhua"), - Pair("Webtoon", "webtoon"), - Pair("Comic", "comic"), - Pair("HQ", "hq"), - Pair("Pornhwa", "pornhwa"), -) - -val categoryModeList = arrayOf( - "Qualquer categoria (OU)" to "or", - "Todas categorias (E)" to "and", -) - -val genreList = arrayOf( - Pair("Academia de Magia", "academia-de-magia"), - Pair("Acadêmica", "academica"), - Pair("Ação", "acao"), - Pair("Adaptação", "adaptacao"), - Pair("Adaptação de Novel", "adaptacao-de-novel"), - Pair("Adultério", "adulterio"), - Pair("Adulto", "adulto"), - Pair("Ahegao", "ahegao"), - Pair("Apocalipse", "apocalipse"), - Pair("App", "app"), - Pair("Artes Marciais", "artes-marciais"), - Pair("Aventura", "aventura"), - Pair("BDSM", "bdsm"), - Pair("Bondage", "bondage"), - Pair("Bullying", "bullying"), - Pair("Bunda Grande", "bunda-grande"), - Pair("Campus", "campus"), - Pair("Cartoon", "cartoon"), - Pair("Casada", "casada"), - Pair("Casamento", "casamento"), - Pair("Club", "club"), - Pair("Comédia", "comedia"), - Pair("Comédia Romântica", "comedia-romantica"), - Pair("Comida", "comida"), - Pair("Cotidiano", "cotidiano"), - Pair("Crime", "crime"), - Pair("Culinária", "culinaria"), - Pair("Cultivo", "cultivo"), - Pair("Curta", "curta"), - Pair("Cyberpunk", "cyberpunk"), - Pair("Dark Romance", "dark-romance"), - Pair("Demônio", "demonio"), - Pair("Demônios", "demonios"), - Pair("Dominação", "dominacao"), - Pair("Doujinshi", "doujinshi"), - Pair("Dragões", "dragoes"), - Pair("Drama", "drama"), - Pair("Ecchi", "ecchi"), - Pair("Escolar", "escolar"), - Pair("Escritório", "escritorio"), - Pair("Esporte", "esporte"), - Pair("Esportes", "esportes"), - Pair("Estratégia", "estrategia"), - Pair("Estudante", "estudante"), - Pair("Exibicionismo", "exibicionismo"), - Pair("Família", "familia"), - Pair("Família Real", "familia-real"), - Pair("Fantasia", "fantasia"), - Pair("Fantasmas", "fantasmas"), - Pair("Fetiche", "fetiche"), - Pair("Ficção Científica", "ficcao-cientifica"), - Pair("Futanari", "futanari"), - Pair("Game", "game"), - Pair("Game System", "game-system"), - Pair("Gangster", "gangster"), - Pair("Gender Bender", "gender-bender"), - Pair("Gênio", "genio"), - Pair("Gore", "gore"), - Pair("Guerra", "guerra"), - Pair("Hardcore", "hardcore"), - Pair("Harem", "harem"), - Pair("Harém Reverso", "harem-reverso"), - Pair("Hentai", "hentai"), - Pair("Híbrido", "hibrido"), - Pair("História", "historia"), - Pair("Histórico", "historico"), - Pair("Horror", "horror"), - Pair("Idols", "idols"), - Pair("Incesto", "incesto"), - Pair("Isekai", "isekai"), - Pair("Jogo", "jogo"), - Pair("Jogos", "jogos"), - Pair("Josei", "josei"), - Pair("Luta", "luta"), - Pair("Máfia", "mafia"), - Pair("Magia", "magia"), - Pair("Magia Escolar", "magia-escolar"), - Pair("Mature", "mature"), - Pair("Mecha", "mecha"), - Pair("Médico", "medico"), - Pair("Metaverso", "metaverso"), - Pair("Milf", "milf"), - Pair("Militar", "militar"), - Pair("Mistério", "misterio"), - Pair("Mitologia", "mitologia"), - Pair("MMORPG", "mmorpg"), - Pair("Monstros", "monstros"), - Pair("Murim", "murim"), - Pair("Música", "musica"), - Pair("Musical", "musical"), - Pair("Nerd", "nerd"), - Pair("Netori", "netori"), - Pair("Obsessão", "obsessao"), - Pair("One Shot", "one-shot"), - Pair("Oneshot", "oneshot"), - Pair("Peitões", "peitoes"), - Pair("Perda de Memória", "perda-de-memoria"), - Pair("Polícia", "policia"), - Pair("Policial", "policial"), - Pair("Portais", "portais"), - Pair("Pós-apocalíptico", "pos-apocaliptico"), - Pair("Profecias", "profecias"), - Pair("Prostituição", "prostituicao"), - Pair("Psicológico", "psicologico"), - Pair("Psicopata", "psicopata"), - Pair("Realidade Virtual", "realidade-virtual"), - Pair("Realismo", "realismo"), - Pair("Reencarnação", "reencarnacao"), - Pair("Reencontro", "reencontro"), - Pair("Regressão", "regressao"), - Pair("Retornado", "retornado"), - Pair("Retorno", "retorno"), - Pair("Revenge", "revenge"), - Pair("Romance", "romance"), - Pair("RPG", "rpg"), - Pair("Sangue", "sangue"), - Pair("School Life", "school-life"), - Pair("Sci-Fi", "sci-fi"), - Pair("Seinen", "seinen"), - Pair("Sem Censura", "sem-censura"), - Pair("Shoujo", "shoujo"), - Pair("Shounen", "shounen"), - Pair("Sistema", "sistema"), - Pair("Sistema de Níveis", "sistema-de-niveis"), - Pair("Slice of Life", "slice-of-life"), - Pair("Smut", "smut"), - Pair("Sobrenatural", "sobrenatural"), - Pair("Sobrevivência", "sobrevivencia"), - Pair("Sombrio", "sombrio"), - Pair("Sports", "sports"), - Pair("Submissão", "submissao"), - Pair("Sugestivo", "sugestivo"), - Pair("Super Poderes", "super-poderes"), - Pair("Supernatural", "supernatural"), - Pair("Superpoderes", "superpoderes"), - Pair("Suspense", "suspense"), - Pair("Tecnológico", "tecnologico"), - Pair("Tela de Sistema", "tela-de-sistema"), - Pair("Terror", "terror"), - Pair("Thriller", "thriller"), - Pair("Tomboy", "tomboy"), - Pair("Torre", "torre"), - Pair("Tóxico", "toxico"), - Pair("Tragédia", "tragedia"), - Pair("Traição", "traicao"), - Pair("Triângulo", "triangulo"), - Pair("Tsundere", "tsundere"), - Pair("Universitários", "universitarios"), - Pair("Vampiro", "vampiro"), - Pair("Vampiros", "vampiros"), - Pair("Viagem no Tempo", "viagem-no-tempo"), - Pair("Vida Adulta", "vida-adulta"), - Pair("Vida Cotidiana", "vida-cotidiana"), - Pair("Vida Escolar", "vida-escolar"), - Pair("Video Game", "video-game"), - Pair("Video Games", "video-games"), - Pair("Vídeo Game", "video-game-1"), - Pair("Vingança", "vinganca"), - Pair("Violência", "violencia"), - Pair("Volta no Tempo", "volta-no-tempo"), - Pair("VRMMO", "vrmmo"), - Pair("Web Comic", "web-comic"), - Pair("Webcomic", "webcomic"), - Pair("Wuxia", "wuxia"), - Pair("Xianxia", "xianxia"), - Pair("Xuanhuan", "xuanhuan"), - Pair("Yuri", "yuri"), - Pair("Zumbis", "zumbis"), -) - -val themeList = arrayOf( - Pair("Animais", "animais"), - Pair("Artes Marciais", "artes-marciais-tema"), - Pair("Culinária", "culinaria-tema"), - Pair("Esporte", "esporte-tema"), - Pair("Fantasia", "fantasia-tema"), - Pair("Ficção Científica", "ficcao-cientifica-tema"), - Pair("Histórico", "historico-tema"), - Pair("Isekai", "isekai-tema"), - Pair("Jogo", "jogo-tema"), - Pair("Militar", "militar-tema"), - Pair("Monstros", "monstros-tema"), - Pair("Música", "musica-tema"), - Pair("Reencarnação", "reencarnacao-tema"), - Pair("Robôs", "robos"), - Pair("Sobrenatural", "sobrenatural-tema"), - Pair("Super Herói", "super-heroi"), - Pair("Vampiros", "vampiros-tema"), - Pair("Vilão", "vilao"), - Pair("Virtual Reality", "virtual-reality"), - Pair("Zumbis", "zumbis-tema"), -) From a2627fbd554fc0e6acd365066f736691ffd53cf0 Mon Sep 17 00:00:00 2001 From: iq8 <266871934+disk-iq8@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:54:09 +0300 Subject: [PATCH 2/2] Add noNsfw pref --- .../extension/pt/spectralscan/NexusToons.kt | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt index eb9af67472c..4ffedef433f 100644 --- a/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt +++ b/src/pt/spectralscan/src/eu/kanade/tachiyomi/extension/pt/spectralscan/NexusToons.kt @@ -69,7 +69,7 @@ abstract class NexusToons : val url = "$baseUrl/api/mangas".toHttpUrl().newBuilder() .addQueryParameter("page", page.toString()) .addQueryParameter("limit", "50") - .addQueryParameter("includeNsfw", "true") + .addQueryParameter("includeNsfw", noNsfw.not().toString()) if (query.isNotBlank()) { url.addQueryParameter("search", query) @@ -115,7 +115,8 @@ abstract class NexusToons : url.addQueryParameter("sortBy", sortBy) url.addQueryParameter("sortOrder", sortOrder) url.addQueryParameter("categoryMode", categoryMode) - if (onlyNsfw) url.addQueryParameter("onlyNsfw", "true") + + if (onlyNsfw && !noNsfw) url.addQueryParameter("onlyNsfw", "true") if (statusList.isNotEmpty()) { url.addQueryParameter("status", statusList.joinToString(",")) @@ -229,12 +230,27 @@ abstract class NexusToons : private val onlyNsfw: Boolean get() = preferences.getBoolean(PREF_ONLY_NSFW_KEY, PREF_ONLY_NSFW_DEFAULT) + private val noNsfw: Boolean + get() = preferences.getBoolean(PREF_NO_NSFW_KEY, PREF_NO_NSFW_DEFAULT) + override fun setupPreferenceScreen(screen: PreferenceScreen) { - SwitchPreferenceCompat(screen.context).apply { + val onlyNsfw = SwitchPreferenceCompat(screen.context).apply { key = PREF_ONLY_NSFW_KEY title = "Mostrar apenas conteúdo +18" summary = "Quando habilitado, exibe apenas conteúdo adulto nas listagens." setDefaultValue(PREF_ONLY_NSFW_DEFAULT) + setEnabled(!noNsfw) + }.also(screen::addPreference) + + SwitchPreferenceCompat(screen.context).apply { + key = PREF_NO_NSFW_KEY + title = "Ocultar adulto (+18)" + summary = "Quando habilitado, oculta conteúdo adulto nas listagens." + setDefaultValue(PREF_NO_NSFW_DEFAULT) + setOnPreferenceChangeListener { _, value -> + onlyNsfw.setEnabled(!(value as Boolean)) + true + } }.also(screen::addPreference) } @@ -287,6 +303,8 @@ abstract class NexusToons : companion object { private const val PREF_ONLY_NSFW_KEY = "pref_only_nsfw" private const val PREF_ONLY_NSFW_DEFAULT = false + private const val PREF_NO_NSFW_KEY = "pref_no_nsfw" + private const val PREF_NO_NSFW_DEFAULT = false private const val CHAPTER_ENCRYPTION_KEY = "NexusToons2026SecretKeyForChapterEncryption!@#\$" } }