New source: YuriNeko (#12688)
* initial commit * add more prefixes for searching + support more urls * verify that id search is actually a number; clearer error messages * Some final touchups + use IOException
This commit is contained in:
		
							parent
							
								
									4b334fda9c
								
							
						
					
					
						commit
						8f64f6480e
					
				
							
								
								
									
										22
									
								
								src/vi/yurineko/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/vi/yurineko/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="eu.kanade.tachiyomi.extension">
 | 
				
			||||||
 | 
					    <application>
 | 
				
			||||||
 | 
					        <activity
 | 
				
			||||||
 | 
					            android:name=".vi.yurineko.YuriNekoUrlActivity"
 | 
				
			||||||
 | 
					            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="yurineko.net"
 | 
				
			||||||
 | 
					                    android:pathPattern="/..*"
 | 
				
			||||||
 | 
					                    android:scheme="https" />
 | 
				
			||||||
 | 
					            </intent-filter>
 | 
				
			||||||
 | 
					        </activity>
 | 
				
			||||||
 | 
					    </application>
 | 
				
			||||||
 | 
					</manifest>
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/vi/yurineko/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/vi/yurineko/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -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"
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 8.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/vi/yurineko/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/vi/yurineko/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/vi/yurineko/res/web_hi_res_512.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 50 KiB  | 
@ -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<ErrorResponseDto>()
 | 
				
			||||||
 | 
					                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<UserDto>(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<MangaListDto>()
 | 
				
			||||||
 | 
					        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<MangasPage> {
 | 
				
			||||||
 | 
					        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<SManga> =
 | 
				
			||||||
 | 
					        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<MangaDto>().toSManga()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chapterListRequest(manga: SManga): Request = GET("$apiUrl${manga.url}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chapterListParse(response: Response): List<SChapter> {
 | 
				
			||||||
 | 
					        val mangaDto = response.parseAs<MangaDto>()
 | 
				
			||||||
 | 
					        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<Page> =
 | 
				
			||||||
 | 
					        response.parseAs<ReadResponseDto>().toPageList()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun imageUrlParse(response: Response): String = throw Exception("Not used")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
 | 
				
			||||||
 | 
					        Filter.Select<String>(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 <reified T> 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:"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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<ChapterDto>,
 | 
				
			||||||
 | 
					    val chapterInfo: ChapterDto,
 | 
				
			||||||
 | 
					    val url: List<String>,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    fun toPageList(): List<Page> = this@ReadResponseDto
 | 
				
			||||||
 | 
					        .url
 | 
				
			||||||
 | 
					        .mapIndexed { index, url -> Page(index, "", url) }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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<TagDto>,
 | 
				
			||||||
 | 
					    val origin: List<TagDto>,
 | 
				
			||||||
 | 
					    val author: List<TagDto>,
 | 
				
			||||||
 | 
					    val tag: List<TagDto>,
 | 
				
			||||||
 | 
					    val couple: List<TagDto>,
 | 
				
			||||||
 | 
					    val lastChapter: ChapterDto? = null,
 | 
				
			||||||
 | 
					    val chapters: List<ChapterDto>? = 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<MangaDto>,
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -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,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -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,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user