diff --git a/src/zh/yidan/AndroidManifest.xml b/src/zh/yidan/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/zh/yidan/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/zh/yidan/build.gradle b/src/zh/yidan/build.gradle new file mode 100644 index 000000000..f4b77e699 --- /dev/null +++ b/src/zh/yidan/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Yidan Girl' + pkgNameSuffix = 'zh.yidan' + extClass = '.Yidan' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/yidan/res/mipmap-hdpi/ic_launcher.png b/src/zh/yidan/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..bb66c1d21 Binary files /dev/null and b/src/zh/yidan/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/yidan/res/mipmap-mdpi/ic_launcher.png b/src/zh/yidan/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..f953246d9 Binary files /dev/null and b/src/zh/yidan/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/yidan/res/mipmap-xhdpi/ic_launcher.png b/src/zh/yidan/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..0d31fd5fb Binary files /dev/null and b/src/zh/yidan/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/yidan/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/yidan/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..9f1a27be2 Binary files /dev/null and b/src/zh/yidan/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/yidan/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/yidan/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4c0aafc21 Binary files /dev/null and b/src/zh/yidan/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/yidan/res/web_hi_res_512.png b/src/zh/yidan/res/web_hi_res_512.png new file mode 100644 index 000000000..863e1eb97 Binary files /dev/null and b/src/zh/yidan/res/web_hi_res_512.png differ diff --git a/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Dto.kt b/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Dto.kt new file mode 100644 index 000000000..84d998610 --- /dev/null +++ b/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Dto.kt @@ -0,0 +1,62 @@ +package eu.kanade.tachiyomi.extension.zh.yidan + +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.Serializable +import org.jsoup.nodes.Entities + +@Serializable +class MangaDto( + private val title: String, + private val mhcate: String?, + private val cateids: String?, + private val author: String?, + private val summary: String?, + private val coverPic: String?, + private val id: Int, +) { + fun toSManga() = SManga.create().apply { + url = id.toString() + title = this@MangaDto.title + author = this@MangaDto.author + description = summary?.trim() + genre = when { + cateids.isNullOrEmpty() -> null + else -> cateids.split(",").joinToString { GENRES[it.toInt()] } + } + status = when { + mhcate.isNullOrEmpty() -> SManga.ONGOING + "5" in mhcate.split(",") -> SManga.COMPLETED + else -> SManga.ONGOING + } + thumbnail_url = coverPic + initialized = true + } +} + +@Serializable +class ChapterDto( + private val createTime: Long, + private val mhid: String, + private val title: String, + private val jiNo: Int +) { + fun toSChapter() = SChapter.create().apply { + url = "$mhid/$jiNo" + name = Entities.unescape(title) + date_upload = createTime * 1000L + } +} + +@Serializable +class PageListDto(private val pics: String) { + val images get() = pics.split(",") +} + +@Serializable +class ListingDto(val list: List, private val total: String) { + val totalCount get() = total.toInt() +} + +@Serializable +class ResponseDto(val data: T) diff --git a/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Filters.kt b/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Filters.kt new file mode 100644 index 000000000..b58bf6bd5 --- /dev/null +++ b/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Filters.kt @@ -0,0 +1,52 @@ +package eu.kanade.tachiyomi.extension.zh.yidan + +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import okhttp3.HttpUrl + +fun getFilterListInternal() = FilterList(ListingFilter(), GenreFilter()) + +fun parseFilters(filters: FilterList, builder: HttpUrl.Builder) { + for (filter in filters) when (filter) { + is ListingFilter -> { + if (filter.state > 0) + builder.addEncodedQueryParameter("mhcate", LISTING_VALUES[filter.state].toString()) + } + is GenreFilter -> { + if (filter.state > 0) + builder.addEncodedQueryParameter("cateid", String.format("%02d", filter.state)) + } + else -> {} + } +} + +class ListingFilter : Filter.Select("分类", LISTINGS) + +val LISTINGS = arrayOf("全部", "排行榜", "新作", "完结漫", "分类0", "分类1", "分类3", "分类7") +val LISTING_VALUES = arrayOf(0, 2, 4, 5, 0, 1, 3, 7) + +class GenreFilter : Filter.Select("标签", GENRES) + +val GENRES = arrayOf( + "全部", + "短漫", // 01 + "甜漫", // 02 + "强强", // 03 + "年下攻", // 04 + "诱受", // 05 + "骨科", // 06 + "调教", // 07 + "健气受", // 08 + "ABO", // 09 + "重生/重逢", // 10 + "财阀", // 11 + "校园", // 12 + "女王受", // 13 + "NP/SM", // 14 + "韩国榜单", // 15 + "高H", // 16 + "架空", // 17 + "娱乐圈", // 18 + "办公室", // 19 + "青梅竹马", // 20 +) diff --git a/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Yidan.kt b/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Yidan.kt new file mode 100644 index 000000000..5f2275767 --- /dev/null +++ b/src/zh/yidan/src/eu/kanade/tachiyomi/extension/zh/yidan/Yidan.kt @@ -0,0 +1,137 @@ +package eu.kanade.tachiyomi.extension.zh.yidan + +import android.app.Application +import androidx.preference.ListPreference +import androidx.preference.PreferenceScreen +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservableSuccess +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 kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +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 + +class Yidan : HttpSource(), ConfigurableSource { + override val name get() = "一耽女孩" + override val lang get() = "zh" + override val supportsLatest get() = true + + override val baseUrl: String + + init { + val mirrors = MIRRORS + val index = Injekt.get().getSharedPreferences("source_$id", 0x0000) + .getString(MIRROR_PREF, "0")!!.toInt().coerceAtMost(mirrors.size - 1) + baseUrl = "https://" + mirrors[index] + } + + override fun headersBuilder() = Headers.Builder() + .add("User-Agent", System.getProperty("http.agent")!!) + + private val json: Json by injectLazy() + + override fun popularMangaRequest(page: Int) = + GET("$baseUrl/prod-api/app-api/vv/mh-list/page?mhcate=2&pageSize=50&pageNo=$page", headers) + + override fun popularMangaParse(response: Response): MangasPage { + val listing: ListingDto = response.parseAs() + val mangas = listing.list.map { it.toSManga() } + val hasNextPage = run { + val url = response.request.url + val pageSize = url.queryParameter("pageSize")!!.toInt() + val pageNumber = url.queryParameter("pageNo")!!.toInt() + pageSize * pageNumber < listing.totalCount + } + return MangasPage(mangas, hasNextPage) + } + + override fun latestUpdatesRequest(page: Int) = + GET("$baseUrl/prod-api/app-api/vv/mh-list/page?mhcate=4&pageSize=50&pageNo=$page", headers) + + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = "$baseUrl/prod-api/app-api/vv/mh-list/page".toHttpUrl().newBuilder() + .apply { if (query.isNotBlank()) addQueryParameter("word", query) } + .apply { parseFilters(filters, this) } + .addEncodedQueryParameter("pageSize", "50") + .addEncodedQueryParameter("pageNo", page.toString()) + .build() + return Request.Builder().url(url).headers(headers).build() + } + + override fun searchMangaParse(response: Response) = popularMangaParse(response) + + // for WebView + override fun mangaDetailsRequest(manga: SManga) = + GET("$baseUrl/#/pages/detail/detail?id=${manga.url}") + + override fun fetchMangaDetails(manga: SManga): Observable { + val request = GET("$baseUrl/prod-api/app-api/vv/mh-list/get?id=${manga.url}", headers) + return client.newCall(request).asObservableSuccess().map { mangaDetailsParse(it) } + } + + override fun mangaDetailsParse(response: Response) = + response.parseAs().toSManga() + + override fun chapterListRequest(manga: SManga) = + GET("$baseUrl/prod-api/app-api/vv/mh-episodes/list?mhid=${manga.url}", headers) + + override fun chapterListParse(response: Response) = + response.parseAs>().map { it.toSChapter() } + + // for WebView + override fun pageListRequest(chapter: SChapter): Request { + val (mangaId, chapterIndex) = chapter.url.split("/") + return GET("$baseUrl/#/pages/read/read?no=$chapterIndex&id=$mangaId") + } + + override fun fetchPageList(chapter: SChapter): Observable> { + val (mangaId, chapterIndex) = chapter.url.split("/") + val url = "$baseUrl/prod-api/app-api/vv/mh-episodes/get?jiNo=$chapterIndex&mhid=$mangaId" + return client.newCall(GET(url, headers)).asObservableSuccess().map { pageListParse(it) } + } + + override fun pageListParse(response: Response) = + response.parseAs().images.mapIndexed { index, url -> + val imageUrl = if (url.startsWith("http")) url else baseUrl + url + Page(index, imageUrl = imageUrl) + } + + override fun imageUrlParse(response: Response) = throw UnsupportedOperationException() + + private inline fun Response.parseAs(): T = use { + json.decodeFromStream>(body!!.byteStream()).data + } + + override fun getFilterList() = getFilterListInternal() + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + ListPreference(screen.context).apply { + val mirrors = MIRRORS + key = MIRROR_PREF + title = "镜像网址(重启生效)" + summary = "%s" + entries = mirrors + entryValues = Array(mirrors.size, Int::toString) + setDefaultValue("0") + }.let(screen::addPreference) + } + + companion object { + private const val MIRROR_PREF = "MIRROR" + private val MIRRORS get() = arrayOf("ydan.cc", "ydan.vip", "dans.cc") + } +}