diff --git a/src/pt/geasscomics/build.gradle b/src/pt/geasscomics/build.gradle new file mode 100644 index 000000000..e13c0153e --- /dev/null +++ b/src/pt/geasscomics/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = 'Geass Comics' + extClass = '.GeassComics' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/pt/geasscomics/res/mipmap-hdpi/ic_launcher.png b/src/pt/geasscomics/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..18f7f1c6c Binary files /dev/null and b/src/pt/geasscomics/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/pt/geasscomics/res/mipmap-mdpi/ic_launcher.png b/src/pt/geasscomics/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..b3192ac65 Binary files /dev/null and b/src/pt/geasscomics/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/pt/geasscomics/res/mipmap-xhdpi/ic_launcher.png b/src/pt/geasscomics/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..26db7510a Binary files /dev/null and b/src/pt/geasscomics/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/pt/geasscomics/res/mipmap-xxhdpi/ic_launcher.png b/src/pt/geasscomics/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..cdba13e88 Binary files /dev/null and b/src/pt/geasscomics/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/pt/geasscomics/res/mipmap-xxxhdpi/ic_launcher.png b/src/pt/geasscomics/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..c968c8bba Binary files /dev/null and b/src/pt/geasscomics/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComics.kt b/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComics.kt new file mode 100644 index 000000000..8fa0fc19f --- /dev/null +++ b/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComics.kt @@ -0,0 +1,118 @@ +package eu.kanade.tachiyomi.extension.pt.geasscomics + +import eu.kanade.tachiyomi.network.GET +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 keiyoushi.utils.parseAs +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response + +class GeassComics : HttpSource() { + + override val name: String = "Geass Comics" + + override val baseUrl: String = "https://geasscomics.xyz" + + private val apiUrl = "https://api.${baseUrl.substringAfterLast("/")}" + + override val lang: String = "pt-BR" + + override val supportsLatest: Boolean = true + + override val client = network.cloudflareClient.newBuilder() + .rateLimitHost(apiUrl.toHttpUrl(), 2, 1) + .build() + + // ========================= Popular ==================================== + + override fun popularMangaRequest(page: Int) = GET("$apiUrl/obras?page=$page&limit=12", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val dto = response.parseAs() + return MangasPage(dto.mangas.map(MangaDto::toSManga), dto.hasNextPage()) + } + + // ========================= Latest ===================================== + + override fun latestUpdatesRequest(page: Int) = GET("$apiUrl/capitulos/recentes?page=$page&limit=150", headers) + + override fun latestUpdatesParse(response: Response): MangasPage { + val dto = response.parseAs() + return MangasPage(dto.mangas.map(SimpleMangaDto::toSManga).distinctBy(SManga::url), false) + } + + // ========================= Search ===================================== + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$apiUrl/obras".toHttpUrl().newBuilder() + .addQueryParameter("page", page.toString()) + .addQueryParameter("limit", "12") + + if (query.isNotBlank()) { + url.addPathSegment("search") + url.addQueryParameter("q", query) + } + + filters.forEach { filter -> + when (filter) { + is GenreList -> { + val genres = filter.state.filter(GenreCheckBox::state) + if (genres.isEmpty()) return@forEach + url.addQueryParameter("generos", genres.joinToString { it.id }) + } + else -> {} + } + } + return GET(url.build(), headers) + } + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + // ========================= Details ==================================== + + override fun getMangaUrl(manga: SManga) = "$baseUrl/manga/${manga.url}" + + override fun mangaDetailsRequest(manga: SManga) = + GET("$apiUrl/obras/${manga.url}/info", headers) + + override fun mangaDetailsParse(response: Response): SManga = + response.parseAs().manga.toSManga() + + // ========================= Chapters =================================== + + override fun getChapterUrl(chapter: SChapter): String = "$baseUrl/${chapter.url}" + + override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga) + + override fun chapterListParse(response: Response): List = + response.parseAs().toChapters() + + // ========================= Pages ====================================== + + override fun pageListRequest(chapter: SChapter) = + GET("$apiUrl${chapter.url.replace("manga", "obras").replace("chapter", "capitulos")}", headers) + + override fun pageListParse(response: Response): List = + response.parseAs().toPages() + + override fun imageUrlParse(response: Response): String = "" + + // ========================= Filters ==================================== + + override fun getFilterList(): FilterList { + return FilterList( + listOf( + GenreList( + title = "Gêneros", + genres = genresList.sortedBy(Genre::name), + ), + ), + ) + } +} diff --git a/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComicsDto.kt b/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComicsDto.kt new file mode 100644 index 000000000..23d282b4f --- /dev/null +++ b/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComicsDto.kt @@ -0,0 +1,131 @@ +package eu.kanade.tachiyomi.extension.pt.geasscomics + +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +class Pagination( + @SerialName("has_next") + val hasNext: Boolean = false, +) + +@Serializable +class PopularDto( + @SerialName("obras") + val mangas: List = emptyList(), + val pagination: Pagination = Pagination(), +) { + fun hasNextPage() = pagination.hasNext +} + +@Serializable +class LatestDto( + @SerialName("recentes") + val mangas: List = emptyList(), +) + +@Serializable +class DetailsDto( + @SerialName("obra") + val manga: MangaDto, +) { + fun toChapters(): List { + return manga.chapters.map { + SChapter.create().apply { + name = it.name + chapter_number = it.number.toFloat() + url = "/manga/${manga.id}/chapter/${it.number}" + } + }.sortedBy(SChapter::chapter_number).reversed() + } +} + +@Serializable +class SimpleMangaDto( + @SerialName("obra_id") + val id: Int, + @SerialName("titulo_obra") + val title: String, + @SerialName("capa") + val thumbnail_url: String, +) { + fun toSManga() = SManga.create().apply { + title = this@SimpleMangaDto.title + thumbnail_url = this@SimpleMangaDto.thumbnail_url + url = this@SimpleMangaDto.id.toString() + } +} + +@Serializable +class MangaDto( + val id: Int, + @SerialName("titulo") + val title: String, + @SerialName("artista") + val artist: String, + @SerialName("autor") + val author: String, + @SerialName("capa") + val thumbnail_url: String, + @SerialName("generos") + val genres: List, + val sinopse: String, + val status: String, + @SerialName("capitulos") + val chapters: List = emptyList(), +) { + + fun toSManga() = SManga.create().apply { + title = this@MangaDto.title + description = this@MangaDto.sinopse + thumbnail_url = this@MangaDto.thumbnail_url + artist = this@MangaDto.artist + author = this@MangaDto.author + genre = genres.joinToString { it.name } + url = this@MangaDto.id.toString() + status = when (this@MangaDto.status) { + "EM ANDAMENTO" -> SManga.ONGOING + "COMPLETO" -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + initialized = true + } + + @Serializable + class Genre( + @SerialName("nome") + val name: String, + ) +} + +@Serializable +class ChapterDto( + val id: Long, + @SerialName("numero") + val number: String, + @SerialName("titulo") + val name: String, +) + +@Serializable +class PageDto( + @SerialName("capitulo") + val chapter: Image, +) { + fun toPages(): List = + chapter.images.mapIndexed { index, source -> Page(index, imageUrl = source.url) } + + @Serializable + class Source( + val url: String, + ) + + @Serializable + class Image( + @SerialName("imagens") + val images: List = emptyList(), + ) +} diff --git a/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComicsFilter.kt b/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComicsFilter.kt new file mode 100644 index 000000000..04f48cc3e --- /dev/null +++ b/src/pt/geasscomics/src/eu/kanade/tachiyomi/extension/pt/geasscomics/GeassComicsFilter.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.extension.pt.geasscomics + +import eu.kanade.tachiyomi.source.model.Filter + +class GenreList(title: String, genres: List) : Filter.Group(title, genres.map { GenreCheckBox(it.name, it.id.toString()) }) +class GenreCheckBox(name: String, val id: String = name) : Filter.CheckBox(name) +class Genre(val id: Int, val name: String) + +val genresList = listOf( + Genre(10, "Isekai"), + Genre(11, "Sistema"), + Genre(12, "Shonen"), + Genre(13, "Shojo"), + Genre(14, "Seinen"), + Genre(15, "Josei"), + Genre(16, "Slice of Life"), + Genre(17, "Horror"), + Genre(18, "Fantasy"), + Genre(19, "Romance"), + Genre(20, "Comedia"), + Genre(21, "Sports"), + Genre(22, "Supernatural"), + Genre(23, "Mystery"), + Genre(24, "Psychological"), + Genre(25, "Aventura"), + Genre(26, "Adulto"), + Genre(27, "Hentai"), + Genre(29, "Harém"), + Genre(30, "Ação"), + Genre(31, "Drama"), + Genre(32, "Escolar"), + Genre(35, "Monstros"), + Genre(36, "Ecchi"), + Genre(37, "Magia"), + Genre(38, "Demônios"), + Genre(40, "Dungeons"), + Genre(41, "Manga"), + Genre(42, "Apocalipse"), + Genre(43, "Manhwa"), + Genre(44, "Ficção Científica"), + Genre(45, "Sugestivo"), + Genre(46, "Loli"), +)