Remove tsuki (#7366)
This commit is contained in:
		
							parent
							
								
									ed426f5e6b
								
							
						
					
					
						commit
						e6753d4ea5
					
				| @ -1,22 +0,0 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <application> | ||||
|         <activity | ||||
|             android:name=".pt.tsukimangas.TsukiMangasUrlActivity" | ||||
|             android:excludeFromRecents="true" | ||||
|             android:exported="true" | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|                 <category android:name="android.intent.category.BROWSABLE" /> | ||||
| 
 | ||||
|                 <data | ||||
|                     android:host="tsuki-mangas.com" | ||||
|                     android:pathPattern="/obra/..*" | ||||
|                     android:scheme="https" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|     </application> | ||||
| </manifest> | ||||
| @ -1,8 +0,0 @@ | ||||
| ext { | ||||
|     extName = 'Tsuki Mangás' | ||||
|     extClass = '.TsukiMangas' | ||||
|     extVersionCode = 7 | ||||
|     isNsfw = true | ||||
| } | ||||
| 
 | ||||
| apply from: "$rootDir/common.gradle" | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.3 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 1.7 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.8 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 6.3 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 8.9 KiB | 
| @ -1,360 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.pt.tsukimangas | ||||
| 
 | ||||
| import android.annotation.SuppressLint | ||||
| import android.app.Application | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
| import android.webkit.WebView | ||||
| import android.webkit.WebViewClient | ||||
| 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 eu.kanade.tachiyomi.util.asJsoup | ||||
| 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.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
| import java.util.concurrent.CountDownLatch | ||||
| import java.util.concurrent.TimeUnit | ||||
| 
 | ||||
| class TsukiMangas : HttpSource() { | ||||
| 
 | ||||
|     override val name = "Tsuki Mangás" | ||||
| 
 | ||||
|     override val baseUrl = "https://tsuki-mangas.com" | ||||
| 
 | ||||
|     private val apiUrl = baseUrl + API_PATH | ||||
| 
 | ||||
|     override val lang = "pt-BR" | ||||
| 
 | ||||
|     override val supportsLatest = true | ||||
| 
 | ||||
|     @delegate:SuppressLint("SetJavaScriptEnabled") | ||||
|     private val token: String by lazy { | ||||
|         val latch = CountDownLatch(1) | ||||
|         var token = "" | ||||
|         Handler(Looper.getMainLooper()).post { | ||||
|             val webView = WebView(Injekt.get<Application>()) | ||||
|             with(webView.settings) { | ||||
|                 javaScriptEnabled = true | ||||
|                 domStorageEnabled = true | ||||
|                 databaseEnabled = true | ||||
|                 blockNetworkImage = true | ||||
|             } | ||||
|             webView.webViewClient = object : WebViewClient() { | ||||
|                 override fun onPageFinished(view: WebView, url: String) { | ||||
|                     val script = "javascript:localStorage['token']" | ||||
|                     view.evaluateJavascript(script) { | ||||
|                         view.apply { | ||||
|                             stopLoading() | ||||
|                             destroy() | ||||
|                         } | ||||
|                         if (it.isBlank() || it in listOf("null", "undefined")) { | ||||
|                             return@evaluateJavascript | ||||
|                         } | ||||
|                         token = it.replace("[\"]+".toRegex(), "") | ||||
|                         latch.countDown() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             webView.loadUrl(baseUrl) | ||||
|         } | ||||
|         latch.await(10, TimeUnit.SECONDS) | ||||
| 
 | ||||
|         token | ||||
|     } | ||||
| 
 | ||||
|     override val client by lazy { | ||||
|         network.client.newBuilder() | ||||
|             .addInterceptor(::apiHeadersInterceptor) | ||||
|             .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("$apiUrl/mangas?page=$page&filter=0", headers) | ||||
| 
 | ||||
|     override fun popularMangaParse(response: Response): MangasPage { | ||||
|         val item = response.parseAs<MangaListDto>() | ||||
|         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("$apiUrl/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<MangasPage> { | ||||
|         return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler | ||||
|             val id = query.removePrefix(PREFIX_SEARCH) | ||||
|             client.newCall(GET("$apiUrl/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 = "$apiUrl/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("$apiUrl/mangas/$id", headers) | ||||
|     } | ||||
| 
 | ||||
|     override fun getMangaUrl(manga: SManga) = baseUrl + manga.url | ||||
| 
 | ||||
|     override fun mangaDetailsParse(response: Response) = SManga.create().apply { | ||||
|         val mangaDto = response.parseAs<CompleteMangaDto>() | ||||
|         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 split = manga.url.split("/").reversed() | ||||
|         val slug = split[0] | ||||
|         val id = split[1] | ||||
| 
 | ||||
|         return GET("$apiUrl/chapters/$id/all#$slug", headers) | ||||
|     } | ||||
| 
 | ||||
|     override fun chapterListParse(response: Response): List<SChapter> { | ||||
|         val parsed = response.parseAs<ChapterListDto>() | ||||
|         val mangaSlug = response.request.url.fragment!! | ||||
|         val mangaId = response.request.url.pathSegments.reversed()[1] | ||||
| 
 | ||||
|         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 = "/leitor/$mangaId/${it.versionId}/$mangaSlug/${it.number}" | ||||
| 
 | ||||
|                 date_upload = it.created_at.orEmpty().toDate() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // =============================== Pages ================================ | ||||
|     override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url | ||||
| 
 | ||||
|     override fun pageListRequest(chapter: SChapter): Request { | ||||
|         val versionId = chapter.url.split("/")[3] | ||||
| 
 | ||||
|         return GET("$apiUrl/chapter/versions/$versionId", headers) | ||||
|     } | ||||
| 
 | ||||
|     override fun pageListParse(response: Response): List<Page> { | ||||
|         val data = response.parseAs<PageListDto>() | ||||
|         val sortedPages = data.pages.sortedBy { it.url.extractPageNumber() } | ||||
|         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 <reified T> Response.parseAs(): T = use { | ||||
|         try { | ||||
|             json.decodeFromStream(it.body.byteStream()) | ||||
|         } catch (_: Exception) { | ||||
|             throw Exception( | ||||
|                 """ | ||||
|                     Contéudo protegido ou foi removido. | ||||
|                     Faça o login na WebView e tente novamente | ||||
|                 """.trimIndent(), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|     } | ||||
| 
 | ||||
|     private val pageNumberRegex = Regex("""(\d+)\.(png|jpg|jpeg|gif|webp)$""") | ||||
| 
 | ||||
|     private fun String.extractPageNumber() = pageNumberRegex | ||||
|         .find(substringBefore("?")) | ||||
|         ?.groupValues | ||||
|         ?.get(1) | ||||
|         ?.toInt() ?: 0 | ||||
| 
 | ||||
|     /** | ||||
|      * 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) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private val apiHeadersRegex = Regex("""headers\.common(?:\.([0-9A-Za-z_]+)|\[['"]([0-9A-Za-z-_]+)['"]])\s*=\s*['"]([a-zA-Z0-9_ :;.,\\/?!(){}\[\]@<>=\-+*#$&`|~^%]+)['"]""") | ||||
| 
 | ||||
|     private val apiHeaders by lazy { | ||||
|         val document = client.newCall(GET(baseUrl, headers)).execute().asJsoup() | ||||
|         val scriptUrl = document.selectFirst("script[src*=index-]")!!.absUrl("src") | ||||
|         val script = client.newCall(GET(scriptUrl, headers)).execute().body.string() | ||||
|         val matches = apiHeadersRegex.findAll(script) | ||||
| 
 | ||||
|         matches.associate { | ||||
|             (it.groups[1] ?: it.groups[2]!!).value to it.groups[3]!!.value | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun apiHeadersInterceptor(chain: Interceptor.Chain): Response { | ||||
|         val request = chain.request() | ||||
| 
 | ||||
|         if (!request.url.encodedPath.startsWith(API_PATH)) { | ||||
|             return chain.proceed(request) | ||||
|         } | ||||
| 
 | ||||
|         val newRequest = request.newBuilder().apply { | ||||
|             apiHeaders.entries.forEach { addHeader(it.key, it.value) } | ||||
|             if (token.isNotBlank()) { | ||||
|                 addHeader("Authorization", "Bearer $token") | ||||
|             } | ||||
|         }.build() | ||||
| 
 | ||||
|         return 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/v3" | ||||
|     } | ||||
| } | ||||
| @ -1,475 +0,0 @@ | ||||
| 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<Pair<String, String>>) : | ||||
|         Filter.Group<Filter.CheckBox>(name, pairs.map { CheckBoxVal(it.first) }) | ||||
| 
 | ||||
|     private class CheckBoxVal(name: String) : Filter.CheckBox(name, false) | ||||
| 
 | ||||
|     private inline fun <reified R> FilterList.parseCheckbox( | ||||
|         options: Array<Pair<String, String>>, | ||||
|     ): Sequence<String> { | ||||
|         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<Pair<String, String>>, | ||||
|     ) : Filter.Select<String>( | ||||
|         displayName, | ||||
|         vals.map { it.first }.toTypedArray(), | ||||
|     ) { | ||||
|         val selected get() = vals[state].second | ||||
|     } | ||||
| 
 | ||||
|     private inline fun <reified R> 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<String> = emptySequence(), | ||||
|         val tags: Sequence<String> = 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<GenresFilter>(GENRES), | ||||
|             filters.parseCheckbox<TagsFilter>(TAGS), | ||||
| 
 | ||||
|             filters.getSelected<FormatFilter>(), | ||||
|             filters.getSelected<AdultFilter>(), | ||||
|             filters.getSelected<ContentFilter>(), | ||||
|             filters.getSelected<StatusFilter>(), | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     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"), | ||||
|     ) | ||||
| } | ||||
| @ -1,41 +0,0 @@ | ||||
| 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/<id>/<item> 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) | ||||
|     } | ||||
| } | ||||
| @ -1,70 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.extension.pt.tsukimangas.dto | ||||
| 
 | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
| 
 | ||||
| @Serializable | ||||
| data class MangaListDto( | ||||
|     val data: List<SimpleMangaDto>, | ||||
|     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<Genre> = emptyList(), | ||||
|     val titles: List<Title> = 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) | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Hellkaros
						Hellkaros