diff --git a/src/pt/tsukimangas/AndroidManifest.xml b/src/pt/tsukimangas/AndroidManifest.xml new file mode 100644 index 000000000..825716396 --- /dev/null +++ b/src/pt/tsukimangas/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/pt/tsukimangas/build.gradle b/src/pt/tsukimangas/build.gradle new file mode 100644 index 000000000..7e38c9877 --- /dev/null +++ b/src/pt/tsukimangas/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Tsuki Mangás' + extClass = '.TsukiMangas' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..8be15d792 Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..fd4f11242 Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0e72a5571 Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..652fffad6 Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..82085d654 Binary files /dev/null and b/src/pt/tsukimangas/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt new file mode 100644 index 000000000..ea3e2a2fb --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangas.kt @@ -0,0 +1,252 @@ +package eu.kanade.tachiyomi.extension.pt.tsukimangas + +import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.ChapterListDto +import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.CompleteMangaDto +import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.MangaListDto +import eu.kanade.tachiyomi.extension.pt.tsukimangas.dto.PageListDto +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.HttpSource +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import uy.kohesive.injekt.injectLazy +import java.text.SimpleDateFormat +import java.util.Locale + +class TsukiMangas : HttpSource() { + + override val name = "Tsuki Mangás" + + override val baseUrl = "https://tsuki-mangas.com" + + private val API_URL = baseUrl + API_PATH + + override val lang = "pt-BR" + + override val supportsLatest = true + + override val client by lazy { + network.client.newBuilder() + .addInterceptor(::imageCdnSwapper) + .rateLimitHost(baseUrl.toHttpUrl(), 2) + .rateLimitHost(MAIN_CDN.toHttpUrl(), 1) + .rateLimitHost(SECONDARY_CDN.toHttpUrl(), 1) + .build() + } + + override fun headersBuilder() = super.headersBuilder().add("Referer", "$baseUrl/") + + private val json: Json by injectLazy() + + // ============================== Popular =============================== + override fun popularMangaRequest(page: Int) = GET("$API_URL/mangas?page=$page&filter=0", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val item = response.parseAs() + val mangas = item.data.map { + SManga.create().apply { + url = "/obra" + it.entryPath + thumbnail_url = baseUrl + it.imagePath + title = it.title + } + } + val hasNextPage = item.page < item.lastPage + return MangasPage(mangas, hasNextPage) + } + + // =============================== Latest =============================== + // Yes, "lastests". High IQ move. + // Also yeah, there's a "?format=0" glued to the page number. Without this, + // the request will blow up with a HTTP 500. + override fun latestUpdatesRequest(page: Int) = GET("$API_URL/home/lastests?page=$page%3Fformat%3D0", headers) + + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + + // =============================== 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("$API_URL/mangas/$id", headers)) + .asObservableSuccess() + .map(::searchMangaByIdParse) + } else { + super.fetchSearchManga(page, query, filters) + } + } + + private fun searchMangaByIdParse(response: Response): MangasPage { + val details = mangaDetailsParse(response) + return MangasPage(listOf(details), false) + } + + override fun getFilterList() = TsukiMangasFilters.FILTER_LIST + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val params = TsukiMangasFilters.getSearchParameters(filters) + val url = "$API_URL/mangas".toHttpUrl().newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("title", query.trim()) + .addIfNotBlank("filter", params.filter) + .addIfNotBlank("format", params.format) + .addIfNotBlank("status", params.status) + .addIfNotBlank("adult_content", params.adult) + .apply { + params.genres.forEach { addQueryParameter("genres[]", it) } + params.tags.forEach { addQueryParameter("tags[]", it) } + }.build() + + return GET(url, headers) + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + + // =========================== Manga Details ============================ + override fun mangaDetailsRequest(manga: SManga): Request { + val id = manga.url.getMangaId() + return GET("$API_URL/mangas/$id", headers) + } + + override fun getMangaUrl(manga: SManga) = baseUrl + manga.url + + override fun mangaDetailsParse(response: Response) = SManga.create().apply { + val mangaDto = response.parseAs() + url = "/obra" + mangaDto.entryPath + thumbnail_url = baseUrl + mangaDto.imagePath + title = mangaDto.title + artist = mangaDto.staff + genre = mangaDto.genres.joinToString { it.genre } + status = parseStatus(mangaDto.status.orEmpty()) + description = buildString { + mangaDto.synopsis?.also { append("$it\n\n") } + if (mangaDto.titles.isNotEmpty()) { + append("Títulos alternativos: ${mangaDto.titles.joinToString { it.title }}") + } + } + } + + private fun parseStatus(status: String) = when (status) { + "Ativo" -> SManga.ONGOING + "Completo" -> SManga.COMPLETED + "Hiato" -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + + // ============================== Chapters ============================== + override fun chapterListRequest(manga: SManga): Request { + val id = manga.url.getMangaId() + return GET("$API_URL/chapters/$id/all", headers) + } + + override fun chapterListParse(response: Response): List { + val parsed = response.parseAs() + + return parsed.chapters.reversed().map { + SChapter.create().apply { + name = "Capítulo ${it.number}" + // Sometimes the "number" attribute have letters or other characters, + // which could ruin the automatic chapter number recognition system. + chapter_number = it.number.trim { char -> !char.isDigit() }.toFloatOrNull() ?: 1F + + url = "$API_PATH/chapter/versions/${it.versionId}" + + date_upload = it.created_at.orEmpty().toDate() + } + } + } + + // =============================== Pages ================================ + override fun pageListParse(response: Response): List { + val data = response.parseAs() + val sortedPages = data.pages.sortedBy { it.url.substringAfterLast("/") } + val host = getImageHost(sortedPages.first().url) + + return sortedPages.mapIndexed { index, item -> + Page(index, imageUrl = host + item.url) + } + } + + /** + * The source normally uses only one CDN per chapter, so we'll try to get + * the correct CDN before loading all pages, leaving the [imageCdnSwapper] + * as the last choice. + */ + private fun getImageHost(path: String): String { + val pageCheck = super.client.newCall(GET(MAIN_CDN + path, headers)).execute() + pageCheck.close() + return when { + !pageCheck.isSuccessful -> SECONDARY_CDN + else -> MAIN_CDN + } + } + + override fun imageUrlParse(response: Response): String { + throw UnsupportedOperationException() + } + + // ============================= Utilities ============================== + private inline fun Response.parseAs(): T = use { + json.decodeFromStream(it.body.byteStream()) + } + + private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder { + if (value.isNotBlank()) addQueryParameter(query, value) + return this + } + + private fun String.getMangaId() = substringAfter("/obra/").substringBefore("/") + + private fun String.toDate(): Long { + return runCatching { DATE_FORMATTER.parse(trim())?.time } + .getOrNull() ?: 0L + } + + /** + * This may sound stupid (because it is), but a similar approach exists + * in the source itself, because they somehow don't know to which server + * each page belongs to. I thought the `server` attribute returned by page + * objects would be enough, but it turns out that it isn't. Day ruined. + */ + private fun imageCdnSwapper(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + return if (response.code != 404) { + response + } else { + response.close() + val url = request.url.toString() + val newUrl = when { + url.startsWith(MAIN_CDN) -> url.replace("$MAIN_CDN/tsuki", SECONDARY_CDN) + url.startsWith(SECONDARY_CDN) -> url.replace(SECONDARY_CDN, "$MAIN_CDN/tsuki") + else -> url + } + + val newRequest = GET(newUrl, request.headers) + chain.proceed(newRequest) + } + } + + companion object { + const val PREFIX_SEARCH = "id:" + + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH) + } + + private const val MAIN_CDN = "https://cdn.tsuki-mangas.com/tsuki" + private const val SECONDARY_CDN = "https://cdn2.tsuki-mangas.com" + private const val API_PATH = "/api/v2" + } +} diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasFilters.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasFilters.kt new file mode 100644 index 000000000..77c5c6c5e --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasFilters.kt @@ -0,0 +1,475 @@ +package eu.kanade.tachiyomi.extension.pt.tsukimangas + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList + +object TsukiMangasFilters { + open class CheckBoxFilterList(name: String, val pairs: Array>) : + Filter.Group(name, pairs.map { CheckBoxVal(it.first) }) + + private class CheckBoxVal(name: String) : Filter.CheckBox(name, false) + + private inline fun FilterList.parseCheckbox( + options: Array>, + ): Sequence { + return (first { it is R } as CheckBoxFilterList).state + .asSequence() + .filter { it.state } + .map { checkbox -> options.find { it.first == checkbox.name }!!.second } + } + + open class SelectFilter( + displayName: String, + val vals: Array>, + ) : Filter.Select( + displayName, + vals.map { it.first }.toTypedArray(), + ) { + val selected get() = vals[state].second + } + + private inline fun FilterList.getSelected(): String { + return (first { it is R } as SelectFilter).selected + } + + internal class GenresFilter : CheckBoxFilterList("Gêneros", GENRES) + internal class TagsFilter : CheckBoxFilterList("Tags", TAGS) + + internal class FormatFilter : SelectFilter("Formato", FORMATS) + internal class AdultFilter : SelectFilter("Mostrar conteúdo adulto", ADULT_OPTIONS) + internal class ContentFilter : SelectFilter("Filtro", CONTENT_FILTER) + internal class StatusFilter : SelectFilter("Status", STATUS) + + internal val FILTER_LIST get() = FilterList( + GenresFilter(), + TagsFilter(), + + FormatFilter(), + AdultFilter(), + ContentFilter(), + StatusFilter(), + ) + + internal data class FilterSearchParams( + val genres: Sequence = emptySequence(), + val tags: Sequence = emptySequence(), + + val format: String = "", + val adult: String = "", + val filter: String = "", + val status: String = "", + ) + + internal fun getSearchParameters(filters: FilterList): FilterSearchParams { + if (filters.isEmpty()) return FilterSearchParams() + + return FilterSearchParams( + filters.parseCheckbox(GENRES), + filters.parseCheckbox(TAGS), + + filters.getSelected(), + filters.getSelected(), + filters.getSelected(), + filters.getSelected(), + ) + } + + private val GENRES = arrayOf( + "4-Koma", + "Adaptação", + "Aliens", + "Animais", + "Antologia", + "Artes Marciais", + "Aventura", + "Ação", + "Colorido por fã", + "Comédia", + "Criado pelo Usuário", + "Crime", + "Cross-dressing", + "Deliquentes", + "Demônios", + "Doujinshi", + "Drama", + "Ecchi", + "Esportes", + "Fantasia", + "Fantasmas", + "Filosófico", + "Gals", + "Ganhador de Prêmio", + "Garotas Monstro", + "Garotas Mágicas", + "Gastronomia", + "Gore", + "Harém", + "Harém Reverso", + "Hentai", + "Histórico", + "Horror", + "Incesto", + "Isekai", + "Jogos Tradicionais", + "Lolis", + "Long Strip", + "Mafia", + "Magia", + "Mecha", + "Medicina", + "Militar", + "Mistério", + "Monstros", + "Música", + "Ninjas", + "Obscenidade", + "Oficialmente Colorido", + "One-shot", + "Policial", + "Psicológico", + "Pós-apocalíptico", + "Realidade Virtual", + "Reencarnação", + "Romance", + "Samurais", + "Sci-Fi", + "Shotas", + "Shoujo Ai", + "Shounen Ai", + "Slice of Life", + "Sobrenatural", + "Sobrevivência", + "Super Herói", + "Thriller", + "Todo Colorido", + "Trabalho de Escritório", + "Tragédia", + "Troca de Gênero", + "Vampiros", + "Viagem no Tempo", + "Vida Escolar", + "Violência Sexual", + "Vídeo Games", + "Webcomic", + "Wuxia", + "Yaoi", + "Yuri", + "Zumbis", + ).map { Pair(it, it) }.toTypedArray() + + private val TAGS = arrayOf( + Pair("4-Koma", "4-Koma"), + Pair("Acromático", "Achromatic"), + Pair("Adoção", "Adoption"), + Pair("Agricultura", "Agriculture"), + Pair("Airsoft", "Airsoft"), + Pair("Alienígenas", "Aliens"), + Pair("Alquimia", "Alchemy"), + Pair("Amadurecimento", "Coming Of Age"), + Pair("Ambiental", "Environmental"), + Pair("Amnésia", "Amnesia"), + Pair("Amor entre Adolescentes", "Teens' Love"), + Pair("Amor entre Homens", "Boys' Love"), + Pair("Anacronismo", "Anachronism"), + Pair("Animais", "Animals"), + Pair("Anjos", "Angels"), + Pair("Anti-Herói", "Anti-Hero"), + Pair("Antologia", "Anthology"), + Pair("Antropomorfismo", "Anthropomorphism"), + Pair("Anúncio Publicitário", "Advertisement"), + Pair("Ao Ar Livre", "Outdoor"), + Pair("Arco e Flecha", "Archery"), + Pair("Armas", "Guns"), + Pair("Artes Marciais", "Martial Arts"), + Pair("Assassinos", "Assassins"), + Pair("Assexual", "Asexual"), + Pair("Astronomia", "Astronomy"), + Pair("Atletismo", "Athletics"), + Pair("Atuação", "Acting"), + Pair("Autobiográfico", "Autobiographical"), + Pair("Aviação", "Aviation"), + Pair("Badminton", "Badminton"), + Pair("Banda", "Band"), + Pair("Bar", "Bar"), + Pair("Barreira de Idioma Estrangeiro", "Foreign Language Barrier"), + Pair("Basquete", "Basketball"), + Pair("Batalha Real", "Battle Royale"), + Pair("Batalha de Cartas", "Card Battle"), + Pair("Beisebol", "Baseball"), + Pair("Biográfico", "Biographical"), + Pair("Bissexual", "Bisexual"), + Pair("Bombeiros", "Firefighters"), + Pair("Boxe", "Boxing"), + Pair("Bruxa", "Witch"), + Pair("Bullying", "Bullying"), + Pair("CGI Completo", "Full CGI"), + Pair("CGI", "CGI"), + Pair("Caligrafia", "Calligraphy"), + Pair("Canibalismo", "Cannibalism"), + Pair("Carros", "Cars"), + Pair("Casamento", "Marriage"), + Pair("Centauro", "Centaur"), + Pair("Chibi", "Chibi"), + Pair("Chuunibyou", "Chuunibyou"), + Pair("Ciborgue", "Cyborg"), + Pair("Ciclismo", "Cycling"), + Pair("Ciclomotores", "Mopeds"), + Pair("Circo", "Circus"), + Pair("Civilização Perdida", "Lost Civilization"), + Pair("Clone", "Clone"), + Pair("Clube Escolar", "School Club"), + Pair("Comida", "Food"), + Pair("Comédia Surrealista", "Surreal Comedy"), + Pair("Conspiração", "Conspiracy"), + Pair("Conto de Fadas", "Fairy Tale"), + Pair("Cor Completa", "Full Color"), + Pair("Cosplay", "Cosplay"), + Pair("Crime", "Crime"), + Pair("Crossover", "Crossover"), + Pair("Cultivo", "Cultivation"), + Pair("Culto", "Cult"), + Pair("Cultura Otaku", "Otaku Culture"), + Pair("Cyberpunk", "Cyberpunk"), + Pair("Dança", "Dancing"), + Pair("Deficiência", "Disability"), + Pair("Delinquentes", "Delinquents"), + Pair("Demônios", "Demons"), + Pair("Denpa", "Denpa"), + Pair("Desenho", "Drawing"), + Pair("Desenvolvimento de Software", "Software Development"), + Pair("Deserto", "Desert"), + Pair("Detetive", "Detective"), + Pair("Deuses", "Gods"), + Pair("Diferença de Idade", "Age Gap"), + Pair("Dinossauros", "Dinosaurs"), + Pair("Distópico", "Dystopian"), + Pair("Donzela do Santuário", "Shrine Maiden"), + Pair("Dragões", "Dragons"), + Pair("Drogas", "Drugs"), + Pair("Dullahan", "Dullahan"), + Pair("E-Sports", "E-Sports"), + Pair("Economia", "Economics"), + Pair("Educacional", "Educational"), + Pair("Elenco Conjunto", "Ensemble Cast"), + Pair("Elenco Principalmente Adolescente", "Primarily Teen Cast"), + Pair("Elenco Principalmente Adulto", "Primarily Adult Cast"), + Pair("Elenco Principalmente Feminino", "Primarily Female Cast"), + Pair("Elenco Principalmente Infantil", "Primarily Child Cast"), + Pair("Elenco Principalmente Masculino", "Primarily Male Cast"), + Pair("Elfo", "Elf"), + Pair("Empregadas", "Maids"), + Pair("Episódico", "Episodic"), + Pair("Ero Guro", "Ero Guro"), + Pair("Escola", "School"), + Pair("Escravidão", "Slavery"), + Pair("Escrita", "Writing"), + Pair("Esgrima", "Fencing"), + Pair("Espaço", "Space"), + Pair("Espionagem", "Espionage"), + Pair("Esqueleto", "Skeleton"), + Pair("Faculdade", "College"), + Pair("Fada", "Fairy"), + Pair("Família Encontrada", "Found Family"), + Pair("Fantasia Urbana", "Urban Fantasy"), + Pair("Fantasma", "Ghost"), + Pair("Filosofia", "Philosophy"), + Pair("Fitness", "Fitness"), + Pair("Flash", "Flash"), + Pair("Fotografia", "Photography"), + Pair("Freira", "Nun"), + Pair("Fugitivo", "Fugitive"), + Pair("Futebol Americano", "American Football"), + Pair("Futebol", "Football"), + Pair("Gangues", "Gangs"), + Pair("Garota Monstro", "Monster Girl"), + Pair("Garotas Bonitinhas Fazendo Coisas Bonitinhas", "Cute Girls Doing Cute Things"), + Pair("Garoto Feminino", "Femboy"), + Pair("Garoto Monstro", "Monster Boy"), + Pair("Garotos Bonitinhos Fazendo Coisas Bonitinhas", "Cute Boys Doing Cute Things"), + Pair("Go", "Go"), + Pair("Goblin", "Goblin"), + Pair("Golfe", "Golf"), + Pair("Gore", "Gore"), + Pair("Guerra", "War"), + Pair("Gyaru", "Gyaru"), + Pair("Gêmeos", "Twins"), + Pair("Handebol", "Handball"), + Pair("Harém Feminino", "Female Harem"), + Pair("Harém Masculino", "Male Harem"), + Pair("Harém com Gêneros Mistos", "Mixed Gender Harem"), + Pair("Henshin", "Henshin"), + Pair("Heterossexual", "Heterosexual"), + Pair("Hikikomori", "Hikikomori"), + Pair("Histórico", "Historical"), + Pair("Horror Corporal", "Body Horror"), + Pair("Horror Cósmico", "Cosmic Horror"), + Pair("Identidades Dissociativas", "Dissociative Identities"), + Pair("Inteligência Artificial", "Artificial Intelligence"), + Pair("Isekai", "Isekai"), + Pair("Iyashikei", "Iyashikei"), + Pair("Jogo da Morte", "Death Game"), + Pair("Jogos Eletrônicos", "Video Games"), + Pair("Jogos de Azar", "Gambling"), + Pair("Judô", "Judo"), + Pair("Kaiju", "Kaiju"), + Pair("Karuta", "Karuta"), + Pair("Kemonomimi", "Kemonomimi"), + Pair("Kuudere", "Kuudere"), + Pair("Lacrosse", "Lacrosse"), + Pair("Literatura Clássica", "Classic Literature"), + Pair("Lobisomem", "Werewolf"), + Pair("Luta Livre", "Wrestling"), + Pair("Luta com Espada", "Swordplay"), + Pair("Luta com Lança", "Spearplay"), + Pair("Líder de Torcida", "Cheerleading"), + Pair("Magia", "Magic"), + Pair("Mahjong", "Mahjong"), + Pair("Manipulação de Memória", "Memory Manipulation"), + Pair("Manipulação do Tempo", "Time Manipulation"), + Pair("Maquiagem", "Makeup"), + Pair("Maria-rapaz", "Tomboy"), + Pair("Masmorra", "Dungeon"), + Pair("Medicina", "Medicine"), + Pair("Mergulho", "Scuba Diving"), + Pair("Meta", "Meta"), + Pair("Militar", "Military"), + Pair("Mitologia", "Mythology"), + Pair("Moda", "Fashion"), + Pair("Mordomo", "Butler"), + Pair("Motocicletas", "Motorcycles"), + Pair("Mudança de Forma", "Shapeshifting"), + Pair("Mulher de Escritório", "Office Lady"), + Pair("Mundo Virtual", "Virtual World"), + Pair("Musical", "Musical"), + Pair("Máfia", "Mafia"), + Pair("Natação", "Swimming"), + Pair("Navios", "Ships"), + Pair("Necromancia", "Necromancy"), + Pair("Nekomimi", "Nekomimi"), + Pair("Ninja", "Ninja"), + Pair("Noir", "Noir"), + Pair("Nudez", "Nudity"), + Pair("Não Ficção", "Non-Fiction"), + Pair("Oiran", "Oiran"), + Pair("Ojou-Sama", "Ojou-Sama"), + Pair("Ordem Acrônica", "Achronological Order"), + Pair("Pandemia", "Pandemic"), + Pair("Parkour", "Parkour"), + Pair("Paródia", "Parody"), + Pair("Patinagem no Gelo", "Ice Skating"), + Pair("Pele Bronzeada", "Tanned Skin"), + Pair("Pesca", "Fishing"), + Pair("Piratas", "Pirates"), + Pair("Polícia", "Police"), + Pair("Política", "Politics"), + Pair("Ponto de Vista", "POV"), + Pair("Prisão", "Prison"), + Pair("Professor(a)", "Teacher"), + Pair("Protagonista Feminina", "Female Protagonist"), + Pair("Protagonista Masculino", "Male Protagonist"), + Pair("Pular no Tempo", "Time Skip"), + Pair("Puppetry", "Puppetry"), + Pair("Pós-Apocalíptico", "Post-Apocalyptic"), + Pair("Pós-Vida", "Afterlife"), + Pair("Pôquer", "Poker"), + Pair("Quimera", "Chimera"), + Pair("Rakugo", "Rakugo"), + Pair("Reabilitação", "Rehabilitation"), + Pair("Realidade Aumentada", "Augmented Reality"), + Pair("Reencarnação", "Reincarnation"), + Pair("Regressão de Idade", "Age Regression"), + Pair("Religião", "Religion"), + Pair("Robô Real", "Real Robot"), + Pair("Robôs", "Robots"), + Pair("Rotoscopia", "Rotoscoping"), + Pair("Rugby", "Rugby"), + Pair("Rural", "Rural"), + Pair("Samurai", "Samurai"), + Pair("Sem Diálogo", "No Dialogue"), + Pair("Sem Gênero", "Agender"), + Pair("Sem-teto", "Homeless"), + Pair("Sereia", "Mermaid"), + Pair("Shogi", "Shogi"), + Pair("Skateboarding", "Skateboarding"), + Pair("Slapstick", "Slapstick"), + Pair("Sobrevivência", "Survival"), + Pair("Steampunk", "Steampunk"), + Pair("Stop Motion", "Stop Motion"), + Pair("Suicídio", "Suicide"), + Pair("Sumô", "Sumo"), + Pair("Super Robô", "Super Robot"), + Pair("Super-herói", "Superhero"), + Pair("Superpoder", "Super Power"), + Pair("Surf", "Surfing"), + Pair("Sátira", "Satire"), + Pair("Súcubo", "Succubus"), + Pair("Tanques", "Tanks"), + Pair("Temas LGBTQ+", "LGBTQ+ Themes"), + Pair("Terrorismo", "Terrorism"), + Pair("Tokusatsu", "Tokusatsu"), + Pair("Tortura", "Torture"), + Pair("Trabalho", "Work"), + Pair("Tragédia", "Tragedy"), + Pair("Transgênero", "Transgender"), + Pair("Travestismo", "Crossdressing"), + Pair("Trens", "Trains"), + Pair("Triângulo Amoroso", "Love Triangle"), + Pair("Troca de Corpos", "Body Swapping"), + Pair("Troca de Gênero", "Gender Bending"), + Pair("Tríades", "Triads"), + Pair("Tsundere", "Tsundere"), + Pair("Tênis de Mesa", "Table Tennis"), + Pair("Tênis", "Tennis"), + Pair("Universo Alternativo", "Alternate Universe"), + Pair("Urbano", "Urban"), + Pair("VTuber", "VTuber"), + Pair("Vampiro", "Vampire"), + Pair("Viagem", "Travel"), + Pair("Vida Familiar", "Family Life"), + Pair("Vikings", "Vikings"), + Pair("Vilã", "Villainess"), + Pair("Vingança", "Revenge"), + Pair("Vôlei", "Volleyball"), + Pair("Wuxia", "Wuxia"), + Pair("Yakuza", "Yakuza"), + Pair("Yandere", "Yandere"), + Pair("Youkai", "Youkai"), + Pair("Yuri", "Yuri"), + Pair("Zumbi", "Zombie"), + Pair("Ídolo", "Idol"), + Pair("Ópera Espacial", "Space Opera"), + Pair("Órfão/Órfã", "Orphan"), + ) + + private val ANY = Pair("Qualquer um", "") + + private val FORMATS = arrayOf( + ANY, + Pair("Mangá", "1"), + Pair("Manhwa", "2"), + Pair("Manhua", "3"), + Pair("Novel", "4"), + ) + + private val ADULT_OPTIONS = arrayOf( + ANY, + Pair("Sim", "1"), + Pair("Não", "0"), + ) + + private val CONTENT_FILTER = arrayOf( + ANY, + Pair("Mais popular", "0"), + Pair("Menos popular", "1"), + Pair("Melhores notas", "2"), + Pair("Piores notas", "3"), + ) + + private val STATUS = arrayOf( + ANY, + Pair("Ativo", "0"), + Pair("Completo", "1"), + Pair("Cancelado", "2"), + Pair("Hiato", "3"), + ) +} diff --git a/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasUrlActivity.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasUrlActivity.kt new file mode 100644 index 000000000..1722468b1 --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/TsukiMangasUrlActivity.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.extension.pt.tsukimangas + +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://tsuki-mangas.com/obra// intents + * and redirects them to the main Tachiyomi process. + */ +class TsukiMangasUrlActivity : 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 id = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${TsukiMangas.PREFIX_SEARCH}$id") + 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/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/dto/TsukiMangasDto.kt b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/dto/TsukiMangasDto.kt new file mode 100644 index 000000000..7207a0150 --- /dev/null +++ b/src/pt/tsukimangas/src/eu/kanade/tachiyomi/extension/pt/tsukimangas/dto/TsukiMangasDto.kt @@ -0,0 +1,70 @@ +package eu.kanade.tachiyomi.extension.pt.tsukimangas.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MangaListDto( + val data: List, + val page: Int, + val lastPage: Int, +) + +@Serializable +data class SimpleMangaDto( + val id: Int, + @SerialName("url") val slug: String, + val title: String, + val poster: String? = null, + val cover: String? = null, +) { + val imagePath = "/img/imgs/${poster ?: cover ?: "nobackground.jpg"}" + val entryPath = "/$id/$slug" +} + +@Serializable +data class CompleteMangaDto( + val id: Int, + @SerialName("url") val slug: String, + + val title: String, + val poster: String? = null, + val cover: String? = null, + val status: String? = null, + val synopsis: String? = null, + val staff: String? = null, + val genres: List = emptyList(), + val titles: List = emptyList(), +) { + val entryPath = "/$id/$slug" + + val imagePath = "/img/imgs/${poster ?: cover ?: "nobackground.jpg"}" + + @Serializable + data class Genre(val genre: String) + + @Serializable + data class Title(val title: String) +} + +@Serializable +data class ChapterListDto(val chapters: List<ChapterDto>) + +@Serializable +data class ChapterDto( + val number: String, + val title: String? = null, + val created_at: String? = null, + private val versions: List<Version>, +) { + @Serializable + data class Version(val id: Int) + + val versionId = versions.first().id +} + +@Serializable +data class PageListDto(val pages: List<PageDto>) + +@Serializable +data class PageDto(val url: String)