diff --git a/src/vi/yurineko/AndroidManifest.xml b/src/vi/yurineko/AndroidManifest.xml new file mode 100644 index 000000000..2e9289da3 --- /dev/null +++ b/src/vi/yurineko/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/src/vi/yurineko/build.gradle b/src/vi/yurineko/build.gradle new file mode 100644 index 000000000..5e76d547a --- /dev/null +++ b/src/vi/yurineko/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'YuriNeko' + pkgNameSuffix = 'vi.yurineko' + extClass = '.YuriNeko' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/vi/yurineko/res/mipmap-hdpi/ic_launcher.png b/src/vi/yurineko/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..c3d989632 Binary files /dev/null and b/src/vi/yurineko/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/vi/yurineko/res/mipmap-mdpi/ic_launcher.png b/src/vi/yurineko/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..ae69ab49f Binary files /dev/null and b/src/vi/yurineko/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/vi/yurineko/res/mipmap-xhdpi/ic_launcher.png b/src/vi/yurineko/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e5f4765d1 Binary files /dev/null and b/src/vi/yurineko/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/vi/yurineko/res/mipmap-xxhdpi/ic_launcher.png b/src/vi/yurineko/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..037377b7a Binary files /dev/null and b/src/vi/yurineko/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/vi/yurineko/res/mipmap-xxxhdpi/ic_launcher.png b/src/vi/yurineko/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..fb0201174 Binary files /dev/null and b/src/vi/yurineko/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/vi/yurineko/res/web_hi_res_512.png b/src/vi/yurineko/res/web_hi_res_512.png new file mode 100644 index 000000000..9023d2c74 Binary files /dev/null and b/src/vi/yurineko/res/web_hi_res_512.png differ diff --git a/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/YuriNeko.kt b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/YuriNeko.kt new file mode 100644 index 000000000..73b46feec --- /dev/null +++ b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/YuriNeko.kt @@ -0,0 +1,402 @@ +package eu.kanade.tachiyomi.extension.vi.yurineko + +import eu.kanade.tachiyomi.extension.vi.yurineko.dto.ErrorResponseDto +import eu.kanade.tachiyomi.extension.vi.yurineko.dto.MangaDto +import eu.kanade.tachiyomi.extension.vi.yurineko.dto.MangaListDto +import eu.kanade.tachiyomi.extension.vi.yurineko.dto.ReadResponseDto +import eu.kanade.tachiyomi.extension.vi.yurineko.dto.UserDto +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.interceptor.rateLimit +import eu.kanade.tachiyomi.source.model.Filter +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.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.CacheControl +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import rx.Observable +import java.io.IOException +import java.net.URLDecoder +import java.util.concurrent.TimeUnit + +class YuriNeko : HttpSource() { + + override val name = "YuriNeko" + + override val baseUrl = "https://yurineko.net" + + override val lang = "vi" + + override val supportsLatest = false + + private val apiUrl = "https://api.yurineko.net" + + override val client = network.cloudflareClient.newBuilder() + .rateLimit(3, 1, TimeUnit.SECONDS) + .addInterceptor { authIntercept(it) } + .addInterceptor { chain -> + val response = chain.proceed(chain.request()) + + if (response.code >= 400 && response.body != null) { + val error = response.parseAs() + response.close() + throw IOException("${error.message}\nĐăng nhập qua WebView và thử lại.") + } + response + }.build() + + override fun headersBuilder() = Headers.Builder().add("Referer", baseUrl) + + private fun authIntercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val cookies = client.cookieJar.loadForRequest(baseUrl.toHttpUrl()) + val authCookie = cookies + .firstOrNull { it.name == "user" } + ?.let { URLDecoder.decode(it.value, "UTF-8") } + ?.let { json.decodeFromString(it) } + ?: return chain.proceed(request) + + val authRequest = request.newBuilder().apply { + addHeader("Authorization", "Bearer ${authCookie.token}") + }.build() + return chain.proceed(authRequest) + } + + override fun popularMangaRequest(page: Int): Request = GET( + url = apiUrl.toHttpUrl().newBuilder().apply { + addPathSegment("lastest2") + addQueryParameter("page", page.toString()) + }.build().toString(), + cache = CacheControl.FORCE_NETWORK + ) + + override fun popularMangaParse(response: Response): MangasPage { + val mangaListDto = response.parseAs() + val currentPage = response.request.url.queryParameter("page")!!.toFloat() + return mangaListDto.toMangasPage(currentPage) + } + + override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used") + + override fun latestUpdatesParse(response: Response): MangasPage = throw Exception("Not used") + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + return when { + query.startsWith(PREFIX_ID_SEARCH) -> { + val id = query.removePrefix(PREFIX_ID_SEARCH).trim() + if (id.toIntOrNull() == null) { + throw Exception("ID tìm kiếm không hợp lệ (phải là một số).") + } + fetchMangaDetails( + SManga.create().apply { + url = "/manga/$id" + } + ) + .map { MangasPage(listOf(it), false) } + } + else -> super.fetchSearchManga(page, query, filters) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return when { + query.startsWith(PREFIX_TAG_SEARCH) || + query.startsWith(PREFIX_COUPLE_SEARCH) || + query.startsWith(PREFIX_DOUJIN_SEARCH) || + query.startsWith(PREFIX_AUTHOR_SEARCH) || + query.startsWith(PREFIX_TEAM_SEARCH) -> { + val items = query.split(":") + val searchType = items[0] + val actualQuery = items[1].trim() + if (actualQuery.toIntOrNull() == null) { + throw Exception("ID tìm kiếm không hợp lệ (phải là một số).") + } + GET( + apiUrl.toHttpUrl().newBuilder().apply { + addPathSegment("searchType") + addQueryParameter("type", searchType) + addQueryParameter("id", actualQuery) + addQueryParameter("page", page.toString()) + }.build().toString() + ) + } + query.isNotEmpty() -> { + GET( + apiUrl.toHttpUrl().newBuilder().apply { + addPathSegment("search") + addQueryParameter("query", query) + addQueryParameter("page", page.toString()) + }.build().toString() + ) + } + else -> { + for (filter in (if (filters.isEmpty()) getFilterList() else filters)) { + when (filter) { + is UriPartFilter -> if (filter.state != 0) { + when (filter.name) { + "Tag" -> return GET( + apiUrl.toHttpUrl().newBuilder().apply { + addPathSegment("searchType") + addQueryParameter("type", "tag") + addQueryParameter("id", filter.toUriPart()) + addQueryParameter("page", page.toString()) + }.build().toString() + ) + else -> continue + } + } + else -> continue + } + } + return popularMangaRequest(page) + } + } + } + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + override fun fetchMangaDetails(manga: SManga): Observable = + client.newCall(GET("$apiUrl${manga.url}")) + .asObservableSuccess() + .map { mangaDetailsParse(it) } + + override fun mangaDetailsRequest(manga: SManga): Request = GET("$baseUrl${manga.url}") + + override fun mangaDetailsParse(response: Response): SManga = + response.parseAs().toSManga() + + override fun chapterListRequest(manga: SManga): Request = GET("$apiUrl${manga.url}") + + override fun chapterListParse(response: Response): List { + val mangaDto = response.parseAs() + val scanlator = mangaDto.team.joinToString(", ") { it.name } + return mangaDto.chapters?.map { it.toSChapter(scanlator) } ?: emptyList() + } + + override fun pageListRequest(chapter: SChapter): Request = GET("$apiUrl${chapter.url}") + + override fun pageListParse(response: Response): List = + response.parseAs().toPageList() + + override fun imageUrlParse(response: Response): String = throw Exception("Not used") + + open class UriPartFilter(displayName: String, private val vals: Array>) : + Filter.Select(displayName, vals.map { it.first }.toTypedArray()) { + fun toUriPart() = vals[state].second + } + + override fun getFilterList() = FilterList( + Filter.Header("Lưu ý rằng không thể vừa tìm kiếm vừa lọc bằng tag cùng lúc."), + Filter.Header("Tìm kiếm sẽ được ưu tiên."), + UriPartFilter("Tag", getGenreList()) + ) + + private fun getGenreList() = arrayOf( + Pair("Sao cũng được", "0"), + Pair("4-koma", "149"), + Pair(">", "306"), + Pair("Action", "113"), + Pair("Adventure", "114"), + Pair("Adult Life", "143"), + Pair("Animal Ears", "175"), + Pair("Age Gap", "179"), + Pair("Anal", "209"), + Pair("Ahegao", "211"), + Pair("Anime", "214"), + Pair("Amnesia", "242"), + Pair("Autobiographical", "255"), + Pair("Alien", "262"), + Pair("Amputee", "277"), + Pair("Assassin", "283"), + Pair("Angel", "298"), + Pair("Abuse", "300"), + Pair("Anilingus", "308"), + Pair("Blushing", "157"), + Pair("Body Swap", "158"), + Pair("Bisexual", "176"), + Pair("Birthday", "194"), + Pair("Big Breasts", "195"), + Pair("Butts", "196"), + Pair("BDSM", "199"), + Pair("Boob Sex", "210"), + Pair("Bath", "226"), + Pair("Bullying", "241"), + Pair("Biting", "270"), + Pair("Blackmail", "280"), + Pair("Biographical", "285"), + Pair("Beach", "289"), + Pair("BHTT", "304"), + Pair("Comedy", "115"), + Pair("College", "145"), + Pair("Co-worker", "180"), + Pair("Childhood Friends", "182"), + Pair("Christmas", "189"), + Pair("Creepy", "220"), + Pair("Childification", "239"), + Pair("Cheating", "267"), + Pair("Clones", "271"), + Pair("Cross-dressing", "288"), + Pair("Chibi", "307"), + Pair("Demon", "116"), + Pair("Drama", "117"), + Pair("Dark Skin", "208"), + Pair("Drunk", "219"), + Pair("Drugs", "236"), + Pair("Disability", "252"), + Pair("Delinquent", "258"), + Pair("Deity", "265"), + Pair("Depressing as fuck", "290"), + Pair("Ecchi", "118"), + Pair("Excuse me WTF?", "161"), + Pair("Exhibitionism", "245"), + Pair("Fantasy", "119"), + Pair("Full Color", "148"), + Pair("FBI Warning!!", "163"), + Pair("Futanari", "201"), + Pair("Food", "232"), + Pair("Feet", "256"), + Pair("Furry", "303"), + Pair("Game", "120"), + Pair("Gender Bender", "121"), + Pair("Glasses", "156"), + Pair("Guro", "206"), + Pair("Ghost", "244"), + Pair("Gyaru", "246"), + Pair("Harem", "122"), + Pair("Historical", "123"), + Pair("Horror", "124"), + Pair("Hints", "152"), + Pair("Het", "160"), + Pair("Halloween", "190"), + Pair("Hypnosis", "254"), + Pair("Height Gap", "281"), + Pair("Hardcore", "292"), + Pair("Isekai", "144"), + Pair("Idol", "169"), + Pair("Incest", "187"), + Pair("Idiot Couple", "282"), + Pair("Introspective", "286"), + Pair("Insane Amounts of Sex", "296"), + Pair("Kuudere", "235"), + Pair("Lỗi: không tìm thấy trai", "153"), + Pair("Love Triangle", "183"), + Pair("Loli", "197"), + Pair("Light Novel", "216"), + Pair("Lactation", "260"), + Pair("Lots of sex", "269"), + Pair("Martial Arts", "125"), + Pair("Mecha", "126"), + Pair("Military", "127"), + Pair("Music", "128"), + Pair("Mystery", "129"), + Pair("Manhua", "146"), + Pair("Manhwa", "147"), + Pair("Moe Paradise", "164"), + Pair("Mahou Shoujo", "168"), + Pair("Maid", "172"), + Pair("Monster Girl", "173"), + Pair("Marriage", "188"), + Pair("Massage", "204"), + Pair("Masturbation", "205"), + Pair("Mangaka", "227"), + Pair("Mermaid", "234"), + Pair("Moderate amounts of sex", "268"), + Pair("Miko", "301"), + Pair("No Text", "150"), + Pair("New Year's", "191"), + Pair("Netorare", "198"), + Pair("NSFW", "229"), + Pair("Ninja", "287"), + Pair("Non-moe art", "302"), + Pair("Office Lady", "174"), + Pair("Oneshot", "218"), + Pair("Official", "222"), + Pair("Orgy", "261"), + Pair("Omegaverse", "276"), + Pair("Parody", "130"), + Pair("Psychological", "131"), + Pair("Pay for Gay", "162"), + Pair("Polyamory", "185"), + Pair("Pocky Game", "212"), + Pair("Prostitution", "240"), + Pair("Player", "257"), + Pair("Prequel", "272"), + Pair("Post-Apocalyptic", "273"), + Pair("Philosophical", "274"), + Pair("R18", "1"), + Pair("Romance", "132"), + Pair("Reversal", "159"), + Pair("Roommates", "181"), + Pair("Rape", "203"), + Pair("Robot", "264"), + Pair("School Life", "133"), + Pair("Sci-Fi", "134"), + Pair("Slice of Life", "137"), + Pair("Sports", "138"), + Pair("Supernatural", "139"), + Pair("Science Babies", "165"), + Pair("Student x Teacher", "166"), + Pair("Siscon", "167"), + Pair("School Girl", "215"), + Pair("Spin-off", "223"), + Pair("Subtext", "231"), + Pair("Sleeping", "249"), + Pair("Sequel", "251"), + Pair("Swimsuits", "263"), + Pair("Stalking", "266"), + Pair("Space", "291"), + Pair("Spanking", "299"), + Pair("Tragedy", "142"), + Pair("Tomboy", "170"), + Pair("Tsundere", "177"), + Pair("Threesome", "184"), + Pair("Twins", "186"), + Pair("Thất Tịch", "193"), + Pair("Toys", "200"), + Pair("Tentacles", "202"), + Pair("Tailsex", "237"), + Pair("Time Travel", "243"), + Pair("Transgender", "284"), + Pair("Vampire", "140"), + Pair("Violence", "141"), + Pair("Valentine", "192"), + Pair("Watersports", "278"), + Pair("Wholesome", "279"), + Pair("Witch", "293"), + Pair("Web Novel", "305"), + Pair("Yuri", "151"), + Pair("Yankee", "171"), + Pair("Yandere", "178"), + Pair("Yuri Crush", "228"), + Pair("Yaoi", "230"), + Pair("Zombies", "238"), + ) + + private val json = Json { + isLenient = true + ignoreUnknownKeys = true + prettyPrint = true + } + + private inline fun Response.parseAs(): T = use { + json.decodeFromString(body?.string().orEmpty()) + } + + companion object { + const val PREFIX_ID_SEARCH = "id:" + const val PREFIX_TAG_SEARCH = "tag:" + const val PREFIX_TEAM_SEARCH = "team:" + const val PREFIX_AUTHOR_SEARCH = "author:" + const val PREFIX_DOUJIN_SEARCH = "origin:" + const val PREFIX_COUPLE_SEARCH = "couple:" + } +} diff --git a/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/YuriNekoUrlActivity.kt b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/YuriNekoUrlActivity.kt new file mode 100644 index 000000000..76ecf4297 --- /dev/null +++ b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/YuriNekoUrlActivity.kt @@ -0,0 +1,47 @@ +package eu.kanade.tachiyomi.extension.vi.yurineko + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import kotlin.system.exitProcess + +class YuriNekoUrlActivity : Activity() { + private fun prefixDeterminer(path: String): String? = when (path) { + "manga" -> YuriNeko.PREFIX_ID_SEARCH + "origin" -> YuriNeko.PREFIX_DOUJIN_SEARCH + "author" -> YuriNeko.PREFIX_AUTHOR_SEARCH + "tag" -> YuriNeko.PREFIX_TAG_SEARCH + "couple" -> YuriNeko.PREFIX_COUPLE_SEARCH + "team" -> YuriNeko.PREFIX_TEAM_SEARCH + else -> null + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val pathSegments = intent?.data?.pathSegments + if (pathSegments != null && + pathSegments.size > 2 && + prefixDeterminer(pathSegments[1]) != null + ) { + val id = pathSegments[2] + try { + startActivity( + Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", "${prefixDeterminer(pathSegments[1])}$id") + putExtra("filter", packageName) + } + ) + } catch (e: ActivityNotFoundException) { + Log.e("YuriNekoUrlActivity", e.toString()) + } + } else { + Log.e("YuriNekoUrlActivity", "Could not parse URI from intent $intent") + } + + finish() + exitProcess(0) + } +} diff --git a/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/ChapterDto.kt b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/ChapterDto.kt new file mode 100644 index 000000000..b41a2980c --- /dev/null +++ b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/ChapterDto.kt @@ -0,0 +1,54 @@ +package eu.kanade.tachiyomi.extension.vi.yurineko.dto + +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +val DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { + timeZone = TimeZone.getTimeZone("Asia/Ho_Chi_Minh") +} + +val FLOATING_NUMBER_REGEX = Regex("""([+-]?(?:[0-9]*[.])?[0-9]+)""") + +@Serializable +data class ChapterDto( + val id: Int, + val name: String, + val date: String? = null, + val mangaID: Int? = null, + val maxID: Int? = null, + val likeCount: Int? = null, +) { + fun toSChapter(teams: String): SChapter = SChapter.create().apply { + val dto = this@ChapterDto + url = "/read/${dto.mangaID}/${dto.id}" + name = dto.name + if (!dto.date.isNullOrEmpty()) { + date_upload = runCatching { + DATE_FORMATTER.parse(dto.date)?.time + }.getOrNull() ?: 0L + } + + val match = FLOATING_NUMBER_REGEX.find(dto.name) + chapter_number = if (dto.name.lowercase().startsWith("vol")) { + match?.groups?.get(2) + } else { + match?.groups?.get(1) + }?.value?.toFloat() ?: -1f + scanlator = teams + } +} + +@Serializable +data class ReadResponseDto( + val listChapter: List, + val chapterInfo: ChapterDto, + val url: List, +) { + fun toPageList(): List = this@ReadResponseDto + .url + .mapIndexed { index, url -> Page(index, "", url) } +} diff --git a/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/MangaDto.kt b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/MangaDto.kt new file mode 100644 index 000000000..8e4ca2b33 --- /dev/null +++ b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/MangaDto.kt @@ -0,0 +1,81 @@ +package eu.kanade.tachiyomi.extension.vi.yurineko.dto + +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.Serializable +import org.jsoup.Jsoup +import org.jsoup.select.Evaluator +import kotlin.math.ceil + +@Serializable +data class MangaDto( + val id: Int, + val originalName: String, + val otherName: String, + val description: String, + val status: Int, + val thumbnail: String, + val type: String, + val lastUpdate: String, + val totalView: Int? = null, + val totalFollow: Int? = null, + val likeCount: Int? = null, + val team: List, + val origin: List, + val author: List, + val tag: List, + val couple: List, + val lastChapter: ChapterDto? = null, + val chapters: List? = null, +) { + fun toSManga(): SManga = SManga.create().apply { + val dto = this@MangaDto + url = "/manga/${dto.id}" + title = dto.originalName + author = dto.author.joinToString(", ") { author -> author.name } + + val descElem = Jsoup.parseBodyFragment(dto.description) + description = if (descElem.select("p").any()) { + Jsoup.parse(dto.description).select("p").joinToString("\n") { + it.run { + select(Evaluator.Tag("br")).prepend("\\n") + this.text().replace("\\n", "\n").replace("\n ", "\n") + } + }.trim() + } else { + dto.description + } + + if (dto.otherName.isNotEmpty()) { + description = "Tên khác: ${dto.otherName}\n\n" + description + } + + genre = dto.tag.joinToString(", ") { tag -> tag.name } + status = when (dto.status) { + 1 -> SManga.UNKNOWN // "Chưa ra mắt" -> Not released + 2 -> SManga.COMPLETED + 3 -> SManga.UNKNOWN // "Sắp ra mắt" -> Upcoming + 4 -> SManga.ONGOING + 5 -> SManga.CANCELLED // "Ngừng dịch" -> source not translating it anymomre + 6 -> SManga.ON_HIATUS + 7 -> SManga.CANCELLED // "Ngừng xuất bản" -> No more publications + else -> SManga.UNKNOWN + } + thumbnail_url = dto.thumbnail + initialized = true + } +} + +@Serializable +data class MangaListDto( + val result: List, + val resultCount: Int, +) { + fun toMangasPage(currentPage: Float = 1f): MangasPage { + val dto = this@MangaListDto + return MangasPage( + dto.result.map { it.toSManga() }, + currentPage + 1f <= ceil(dto.resultCount.toFloat() / 20f) + ) + } +} diff --git a/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/MiscDto.kt b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/MiscDto.kt new file mode 100644 index 000000000..082dbfd93 --- /dev/null +++ b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/MiscDto.kt @@ -0,0 +1,22 @@ +package eu.kanade.tachiyomi.extension.vi.yurineko.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorResponseDto( + val message: String? = null, +) + +@Serializable +data class UserDto( + val id: Int, + val name: String, + val email: String, + val avatar: String, + val role: Int, + val money: Int, + val username: String, + val isBanned: Int, + val isPremium: Int, + val token: String, +) diff --git a/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/TagDto.kt b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/TagDto.kt new file mode 100644 index 000000000..0ab72f512 --- /dev/null +++ b/src/vi/yurineko/src/eu/kanade/tachiyomi/extension/vi/yurineko/dto/TagDto.kt @@ -0,0 +1,11 @@ +package eu.kanade.tachiyomi.extension.vi.yurineko.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class TagDto( + val id: Int, + val name: String, + val url: String, + val origin: String? = null, +)