Add Zaimanhua (#5092)
This commit is contained in:
		
							parent
							
								
									59e9181bf9
								
							
						
					
					
						commit
						5f905c713d
					
				
							
								
								
									
										7
									
								
								src/zh/zaimanhua/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/zh/zaimanhua/build.gradle
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
ext {
 | 
			
		||||
    extName = 'Zaimanhua'
 | 
			
		||||
    extClass = '.Zaimanhua'
 | 
			
		||||
    extVersionCode = 1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
apply from: "$rootDir/common.gradle"
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 2.7 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/zh/zaimanhua/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 6.0 KiB  | 
@ -0,0 +1,30 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.zaimanhua
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import kotlinx.serialization.decodeFromString
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
 | 
			
		||||
val json: Json by injectLazy()
 | 
			
		||||
 | 
			
		||||
inline fun <reified T> Response.parseAs(): T {
 | 
			
		||||
    return json.decodeFromString(body.string())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun parseStatus(status: String): Int = when (status) {
 | 
			
		||||
    "连载中" -> SManga.ONGOING
 | 
			
		||||
    "已完结" -> SManga.COMPLETED
 | 
			
		||||
    else -> SManga.UNKNOWN
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val chapterNameRegex = Regex("""(?:连载版?)?(\d[.\d]*)([话卷])?""")
 | 
			
		||||
 | 
			
		||||
fun String.formatChapterName(): String {
 | 
			
		||||
    val match = chapterNameRegex.matchEntire(this) ?: return this
 | 
			
		||||
    val (number, optionalType) = match.destructured
 | 
			
		||||
    val type = optionalType.ifEmpty { "话" }
 | 
			
		||||
    return "第$number$type"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun String.formatList() = replace("/", ", ")
 | 
			
		||||
@ -0,0 +1,225 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.zaimanhua
 | 
			
		||||
 | 
			
		||||
import android.app.Application
 | 
			
		||||
import android.content.SharedPreferences
 | 
			
		||||
import androidx.preference.EditTextPreference
 | 
			
		||||
import androidx.preference.ListPreference
 | 
			
		||||
import androidx.preference.PreferenceScreen
 | 
			
		||||
import eu.kanade.tachiyomi.network.GET
 | 
			
		||||
import eu.kanade.tachiyomi.network.POST
 | 
			
		||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
			
		||||
import eu.kanade.tachiyomi.source.ConfigurableSource
 | 
			
		||||
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 okhttp3.FormBody
 | 
			
		||||
import okhttp3.Headers
 | 
			
		||||
import okhttp3.HttpUrl
 | 
			
		||||
import okhttp3.HttpUrl.Companion.toHttpUrl
 | 
			
		||||
import okhttp3.Interceptor
 | 
			
		||||
import okhttp3.OkHttpClient
 | 
			
		||||
import okhttp3.Request
 | 
			
		||||
import okhttp3.RequestBody
 | 
			
		||||
import okhttp3.Response
 | 
			
		||||
import uy.kohesive.injekt.Injekt
 | 
			
		||||
import uy.kohesive.injekt.api.get
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
 | 
			
		||||
class Zaimanhua : HttpSource(), ConfigurableSource {
 | 
			
		||||
    override val lang = "zh"
 | 
			
		||||
    override val supportsLatest = true
 | 
			
		||||
 | 
			
		||||
    override val name = "再漫画"
 | 
			
		||||
    override val baseUrl = "https://manhua.zaimanhua.com"
 | 
			
		||||
    private val apiUrl = "https://v4api.zaimanhua.com/app/v1"
 | 
			
		||||
    private val accountApiUrl = "https://account-api.zaimanhua.com/v1"
 | 
			
		||||
 | 
			
		||||
    private val preferences: SharedPreferences =
 | 
			
		||||
        Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
 | 
			
		||||
 | 
			
		||||
    override val client: OkHttpClient =
 | 
			
		||||
        network.client.newBuilder().rateLimit(5).addInterceptor(::authIntercept).build()
 | 
			
		||||
 | 
			
		||||
    private fun authIntercept(chain: Interceptor.Chain): Response {
 | 
			
		||||
        val request = chain.request()
 | 
			
		||||
        if (request.url.host != "v4api.zaimanhua.com" || !request.headers["authorization"].isNullOrBlank()) {
 | 
			
		||||
            return chain.proceed(request)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var token: String = preferences.getString("TOKEN", "")!!
 | 
			
		||||
        if (token.isBlank() || !isValid(token)) {
 | 
			
		||||
            val username = preferences.getString("USERNAME", "")!!
 | 
			
		||||
            val password = preferences.getString("PASSWORD", "")!!
 | 
			
		||||
            token = getToken(username, password)
 | 
			
		||||
            if (token.isBlank()) {
 | 
			
		||||
                preferences.edit().putString("TOKEN", "").apply()
 | 
			
		||||
                preferences.edit().putString("USERNAME", "").apply()
 | 
			
		||||
                preferences.edit().putString("PASSWORD", "").apply()
 | 
			
		||||
                return chain.proceed(request)
 | 
			
		||||
            } else {
 | 
			
		||||
                preferences.edit().putString("TOKEN", token).apply()
 | 
			
		||||
                apiHeaders = apiHeaders.newBuilder().setToken(token).build()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        val authRequest = request.newBuilder().apply {
 | 
			
		||||
            header("authorization", "Bearer $token")
 | 
			
		||||
        }.build()
 | 
			
		||||
        return chain.proceed(authRequest)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun Headers.Builder.setToken(token: String): Headers.Builder = apply {
 | 
			
		||||
        if (token.isNotBlank()) set("authorization", "Bearer $token")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private var apiHeaders = headersBuilder().setToken(preferences.getString("TOKEN", "")!!).build()
 | 
			
		||||
 | 
			
		||||
    private fun isValid(token: String): Boolean {
 | 
			
		||||
        val response = client.newCall(
 | 
			
		||||
            GET(
 | 
			
		||||
                "$accountApiUrl/userInfo/get",
 | 
			
		||||
                headersBuilder().setToken(token).build(),
 | 
			
		||||
            ),
 | 
			
		||||
        ).execute().parseAs<ResponseDto<UserDto>>()
 | 
			
		||||
        return response.errno == 0
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getToken(username: String, password: String): String {
 | 
			
		||||
        if (username.isBlank() || password.isBlank()) return ""
 | 
			
		||||
        val passwordEncoded =
 | 
			
		||||
            MessageDigest.getInstance("MD5").digest(password.toByteArray(Charsets.UTF_8))
 | 
			
		||||
                .joinToString("") { "%02x".format(it) }
 | 
			
		||||
        val formBody: RequestBody = FormBody.Builder().addEncoded("username", username)
 | 
			
		||||
            .addEncoded("passwd", passwordEncoded).build()
 | 
			
		||||
        val response = client.newCall(
 | 
			
		||||
            POST(
 | 
			
		||||
                "$accountApiUrl/login/passwd",
 | 
			
		||||
                headers,
 | 
			
		||||
                formBody,
 | 
			
		||||
            ),
 | 
			
		||||
        ).execute().parseAs<ResponseDto<UserDto>>()
 | 
			
		||||
        return response.data.user?.token ?: ""
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Detail
 | 
			
		||||
    // path: "/comic/detail/mangaId"
 | 
			
		||||
    override fun mangaDetailsRequest(manga: SManga): Request =
 | 
			
		||||
        GET("$apiUrl/comic/detail/${manga.url}", apiHeaders)
 | 
			
		||||
 | 
			
		||||
    override fun mangaDetailsParse(response: Response): SManga {
 | 
			
		||||
        val result = response.parseAs<ResponseDto<DataWrapperDto<MangaDto>>>()
 | 
			
		||||
        if (result.errmsg.isNotBlank()) {
 | 
			
		||||
            throw Exception(result.errmsg)
 | 
			
		||||
        } else {
 | 
			
		||||
            return result.data.data!!.toSManga()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Chapter
 | 
			
		||||
    override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
 | 
			
		||||
 | 
			
		||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
			
		||||
        val result = response.parseAs<ResponseDto<DataWrapperDto<MangaDto>>>()
 | 
			
		||||
        if (result.errmsg.isNotBlank()) {
 | 
			
		||||
            throw Exception(result.errmsg)
 | 
			
		||||
        } else {
 | 
			
		||||
            return result.data.data!!.parseChapterList()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
 | 
			
		||||
 | 
			
		||||
    // PageList
 | 
			
		||||
    // path: "/comic/chapter/mangaId/chapterId"
 | 
			
		||||
    override fun pageListRequest(chapter: SChapter) =
 | 
			
		||||
        GET("$apiUrl/comic/chapter/${chapter.url}", apiHeaders)
 | 
			
		||||
 | 
			
		||||
    override fun pageListParse(response: Response): List<Page> {
 | 
			
		||||
        val result = response.parseAs<ResponseDto<DataWrapperDto<ChapterImagesDto>>>()
 | 
			
		||||
        if (result.errmsg.isNotBlank()) {
 | 
			
		||||
            throw Exception(result.errmsg)
 | 
			
		||||
        } else {
 | 
			
		||||
            return result.data.data!!.images.mapIndexed { index, it ->
 | 
			
		||||
                Page(index, imageUrl = it)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Popular
 | 
			
		||||
    private fun rankApiUrl(): HttpUrl.Builder =
 | 
			
		||||
        "$apiUrl/comic/rank/list".toHttpUrl().newBuilder().addQueryParameter("by_time", "3")
 | 
			
		||||
            .addQueryParameter("tag_id", "0").addQueryParameter("rank_type", "0")
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaRequest(page: Int): Request = GET(
 | 
			
		||||
        rankApiUrl().apply {
 | 
			
		||||
            addQueryParameter("page", page.toString())
 | 
			
		||||
        }.build(),
 | 
			
		||||
        apiHeaders,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response)
 | 
			
		||||
 | 
			
		||||
    // Search
 | 
			
		||||
    private fun searchApiUrl(): HttpUrl.Builder =
 | 
			
		||||
        "$apiUrl/search/index".toHttpUrl().newBuilder().addQueryParameter("source", "0")
 | 
			
		||||
            .addQueryParameter("size", "20")
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET(
 | 
			
		||||
        searchApiUrl().apply {
 | 
			
		||||
            addQueryParameter("keyword", query)
 | 
			
		||||
            addQueryParameter("page", page.toString())
 | 
			
		||||
        }.build(),
 | 
			
		||||
        apiHeaders,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    override fun searchMangaParse(response: Response): MangasPage =
 | 
			
		||||
        response.parseAs<ResponseDto<PageDto>>().data.toMangasPage()
 | 
			
		||||
 | 
			
		||||
    // Latest
 | 
			
		||||
    // "$apiUrl/comic/update/list/1/$page" is same content
 | 
			
		||||
    override fun latestUpdatesRequest(page: Int): Request =
 | 
			
		||||
        GET("$apiUrl/comic/update/list/0/$page", apiHeaders)
 | 
			
		||||
 | 
			
		||||
    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
			
		||||
        val mangas = response.parseAs<ResponseDto<List<PageItemDto>>>().data
 | 
			
		||||
        return MangasPage(mangas.map { it.toSManga() }, true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun setupPreferenceScreen(screen: PreferenceScreen) {
 | 
			
		||||
        ListPreference(screen.context).apply {
 | 
			
		||||
            EditTextPreference(screen.context).apply {
 | 
			
		||||
                key = "USERNAME"
 | 
			
		||||
                title = "用户名"
 | 
			
		||||
                summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置"
 | 
			
		||||
                setOnPreferenceChangeListener { _, _ ->
 | 
			
		||||
                    // clean token after username/password changed
 | 
			
		||||
                    preferences.edit().putString("TOKEN", "").apply()
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            }.let(screen::addPreference)
 | 
			
		||||
 | 
			
		||||
            EditTextPreference(screen.context).apply {
 | 
			
		||||
                key = "PASSWORD"
 | 
			
		||||
                title = "密码"
 | 
			
		||||
                summary = "该配置被修改后,会清空令牌(Token)以便重新登录;如果登录失败,会清空该配置"
 | 
			
		||||
                setOnPreferenceChangeListener { _, _ ->
 | 
			
		||||
                    // clean token after username/password changed
 | 
			
		||||
                    preferences.edit().putString("TOKEN", "").apply()
 | 
			
		||||
                    true
 | 
			
		||||
                }
 | 
			
		||||
            }.let(screen::addPreference)
 | 
			
		||||
 | 
			
		||||
            EditTextPreference(screen.context).apply {
 | 
			
		||||
                key = "TOKEN"
 | 
			
		||||
                title = "令牌(Token)"
 | 
			
		||||
                summary = "当前登录状态:${
 | 
			
		||||
                if (preferences.getString("TOKEN", "").isNullOrEmpty()) "未登录" else "已登录"
 | 
			
		||||
                }\n填写用户名和密码后,不会立刻尝试登录,会在下次请求时自动尝试"
 | 
			
		||||
 | 
			
		||||
                setEnabled(false)
 | 
			
		||||
            }.let(screen::addPreference)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,144 @@
 | 
			
		||||
package eu.kanade.tachiyomi.extension.zh.zaimanhua
 | 
			
		||||
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.MangasPage
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
			
		||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
			
		||||
import kotlinx.serialization.SerialName
 | 
			
		||||
import kotlinx.serialization.Serializable
 | 
			
		||||
import kotlinx.serialization.json.JsonNames
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class MangaDto(
 | 
			
		||||
    private val id: Int,
 | 
			
		||||
    private val title: String,
 | 
			
		||||
    private val cover: String,
 | 
			
		||||
    private val description: String? = null,
 | 
			
		||||
    private val types: List<TagDto>,
 | 
			
		||||
    private val status: List<TagDto>,
 | 
			
		||||
    private val authors: List<TagDto>,
 | 
			
		||||
    @SerialName("chapters")
 | 
			
		||||
    private val chapterGroups: List<ChapterGroupDto>,
 | 
			
		||||
) {
 | 
			
		||||
    fun toSManga() = SManga.create().apply {
 | 
			
		||||
        url = id.toString()
 | 
			
		||||
        title = this@MangaDto.title
 | 
			
		||||
        author = authors.joinToString { it.name }
 | 
			
		||||
        description = this@MangaDto.description
 | 
			
		||||
        genre = types.joinToString { it.name }
 | 
			
		||||
        status = parseStatus(this@MangaDto.status[0].name)
 | 
			
		||||
        thumbnail_url = cover
 | 
			
		||||
        initialized = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun parseChapterList(): List<SChapter> {
 | 
			
		||||
        val mangaId = id.toString()
 | 
			
		||||
        val size = chapterGroups.sumOf { it.size }
 | 
			
		||||
        return chapterGroups.flatMapTo(ArrayList(size)) {
 | 
			
		||||
            it.toSChapterList(mangaId)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ChapterGroupDto(
 | 
			
		||||
    private val title: String,
 | 
			
		||||
    private val data: List<ChapterDto>,
 | 
			
		||||
) {
 | 
			
		||||
    fun toSChapterList(mangaId: String): List<SChapter> {
 | 
			
		||||
        val groupName = title
 | 
			
		||||
        val isDefaultGroup = groupName == "连载"
 | 
			
		||||
        return data.map {
 | 
			
		||||
            it.toSChapterInternal().apply {
 | 
			
		||||
                url = "$mangaId/$url"
 | 
			
		||||
                if (!isDefaultGroup) scanlator = groupName
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val size get() = data.size
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ChapterDto(
 | 
			
		||||
    @SerialName("chapter_id")
 | 
			
		||||
    private val id: Int,
 | 
			
		||||
    @SerialName("chapter_title")
 | 
			
		||||
    private val name: String,
 | 
			
		||||
    @SerialName("updatetime")
 | 
			
		||||
    private val updateTime: Long = 0,
 | 
			
		||||
) {
 | 
			
		||||
    fun toSChapterInternal() = SChapter.create().apply {
 | 
			
		||||
        url = id.toString()
 | 
			
		||||
        name = this@ChapterDto.name.formatChapterName()
 | 
			
		||||
        date_upload = updateTime * 1000
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ChapterImagesDto(
 | 
			
		||||
    @SerialName("page_url_hd")
 | 
			
		||||
    val images: List<String>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class PageDto(
 | 
			
		||||
    private val list: List<PageItemDto>?,
 | 
			
		||||
    private val page: Int,
 | 
			
		||||
    private val size: Int,
 | 
			
		||||
    private val total: Int,
 | 
			
		||||
) {
 | 
			
		||||
    fun toMangasPage(): MangasPage {
 | 
			
		||||
        if (list.isNullOrEmpty()) throw Exception("漫画结果为空,请检查输入")
 | 
			
		||||
        val hasNextPage = page * size < total
 | 
			
		||||
        return MangasPage(list.map { it.toSManga() }, hasNextPage)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class PageItemDto(
 | 
			
		||||
    @JsonNames("comic_id")
 | 
			
		||||
    private val id: Int,
 | 
			
		||||
    private val title: String,
 | 
			
		||||
    private val authors: String = "",
 | 
			
		||||
    private val status: String,
 | 
			
		||||
    private val cover: String,
 | 
			
		||||
    private val types: String,
 | 
			
		||||
) {
 | 
			
		||||
    fun toSManga() = SManga.create().apply {
 | 
			
		||||
        url = this@PageItemDto.id.toString()
 | 
			
		||||
        title = this@PageItemDto.title
 | 
			
		||||
        author = authors.formatList()
 | 
			
		||||
        genre = types.formatList()
 | 
			
		||||
        status = parseStatus(this@PageItemDto.status)
 | 
			
		||||
        thumbnail_url = cover
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class TagDto(
 | 
			
		||||
    @SerialName("tag_name")
 | 
			
		||||
    val name: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class UserDto(
 | 
			
		||||
    @JsonNames("userInfo")
 | 
			
		||||
    val user: UserInfoDto?,
 | 
			
		||||
) {
 | 
			
		||||
    @Serializable
 | 
			
		||||
    class UserInfoDto(
 | 
			
		||||
        val token: String,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class DataWrapperDto<T>(
 | 
			
		||||
    val data: T?,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@Serializable
 | 
			
		||||
class ResponseDto<T>(
 | 
			
		||||
    val errno: Int = 0,
 | 
			
		||||
    val errmsg: String = "",
 | 
			
		||||
    val data: T,
 | 
			
		||||
)
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user