Remove Bilibili Manga and Kuaikanmanhua (#10255)

This commit is contained in:
stevenyomi 2025-08-24 09:54:20 +00:00 committed by Draff
parent 0e9e55b945
commit c0e22429bb
Signed by: Draff
GPG Key ID: E8A89F3211677653
23 changed files with 0 additions and 1461 deletions

View File

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".zh.bilibilimanga.BilibiliUrlActivity"
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:scheme="https" />
<data android:host="manga.bilibili.com" />
<data android:pathPattern="/detail/mc..*" />
<data android:pathPattern="/m/detail/mc..*" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,46 +0,0 @@
# Bilibili
Table of Content
- [FAQ](#FAQ)
- [Why are some chapters missing?](#why-are-some-chapters-missing)
- [Guides](#Guides)
- [Reading already paid chapters](#reading-already-paid-chapters)
Don't find the question you are looking for? Go check out our general FAQs and Guides
over at [Extension FAQ] or [Getting Started].
[Extension FAQ]: https://tachiyomi.org/help/faq/#extensions
[Getting Started]: https://tachiyomi.org/help/guides/getting-started/#installation
## FAQ
### Why are some chapters missing?
Bilibili now have series with paid chapters. These will be filtered out from
the chapter list by default if you didn't buy it before or if you're not signed in.
To sign in with your existing account, follow the guide available above.
## Guides
### Reading already paid chapters
The **Bilibili Comics** sources allows the reading of paid chapters in your account.
Follow the following steps to be able to sign in and get access to them:
1. Open the popular or latest section of the source.
2. Open the WebView by clicking the button with a globe icon.
3. Do the login with your existing account *(read the observations section)*.
4. Close the WebView and refresh the chapter list of the titles
you want to read the already paid chapters.
#### Observations
- Sign in with your Google account is not supported due to WebView restrictions
access that Google have. **You need to have a simple account in order to be able
to login via WebView**.
- You may sometime face the *"Failed to refresh the token"* error. To fix it,
you just need to open the WebView, await for the website to completely load.
After that, you can close the WebView and try again.
- The extension **will not** bypass any payment requirement. You still do need
to buy the chapters you want to read or wait until they become available and
added to your account.

View File

@ -1,7 +0,0 @@
ext {
extName = 'BILIBILI MANGA'
extClass = '.BilibiliManga'
extVersionCode = 13
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,520 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
import android.content.SharedPreferences
import android.util.Base64
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import org.jsoup.Jsoup
import uy.kohesive.injekt.injectLazy
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.text.SimpleDateFormat
import java.util.Locale
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
abstract class Bilibili(
override val name: String,
final override val baseUrl: String,
final override val lang: String,
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(::expiredImageTokenIntercept)
.addInterceptor(::decryptImageIntercept)
.rateLimitHost(baseUrl.toHttpUrl(), 1)
.rateLimitHost(CDN_URL.toHttpUrl(), 2)
.rateLimitHost(MODIFIED_CDN_URL.toHttpUrl(), 2)
.rateLimitHost(COVER_CDN_URL.toHttpUrl(), 2)
.build()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Accept", ACCEPT_JSON)
.add("Origin", baseUrl)
.add("Referer", "$baseUrl/")
protected open val intl by lazy { BilibiliIntl(lang) }
private val apiLang: String = when (lang) {
BilibiliIntl.SIMPLIFIED_CHINESE -> "cn"
else -> lang
}
protected open val defaultPopularSort: Int = 0
protected open val defaultLatestSort: Int = 1
private val preferences: SharedPreferences by getPreferencesLazy()
protected val json: Json by injectLazy()
protected open val signedIn: Boolean = false
override fun popularMangaRequest(page: Int): Request = searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("", getAllSortOptions(), defaultPopularSort),
),
)
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request = searchMangaRequest(
page = page,
query = "",
filters = FilterList(
SortFilter("", getAllSortOptions(), defaultLatestSort),
),
)
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
ID_SEARCH_PATTERN.matchEntire(query)?.let {
val (id) = it.destructured
val temporaryManga = SManga.create().apply { url = "/detail/mc$id" }
return mangaDetailsRequest(temporaryManga)
}
val price = filters.firstInstanceOrNull<PriceFilter>()?.state ?: 0
val jsonPayload = buildJsonObject {
put("area_id", filters.firstInstanceOrNull<AreaFilter>()?.selected?.id ?: -1)
put("is_finish", filters.firstInstanceOrNull<StatusFilter>()?.state?.minus(1) ?: -1)
put("is_free", if (price == 0) -1 else price)
put("order", filters.firstInstanceOrNull<SortFilter>()?.selected?.id ?: 0)
put("page_num", page)
put("page_size", if (query.isBlank()) POPULAR_PER_PAGE else SEARCH_PER_PAGE)
put("style_id", filters.firstInstanceOrNull<GenreFilter>()?.selected?.id ?: -1)
put("style_prefer", "[]")
if (query.isNotBlank()) {
put("need_shield_prefer", true)
put("key_word", query)
}
}
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val refererUrl = if (query.isBlank()) {
"$baseUrl/genre"
} else {
"$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("keyword", query)
.toString()
}
val newHeaders = headersBuilder()
.set("Referer", refererUrl)
.build()
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/".toHttpUrl().newBuilder()
.addPathSegment(if (query.isBlank()) "ClassPage" else "Search")
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
override fun searchMangaParse(response: Response): MangasPage {
val requestUrl = response.request.url.toString()
if (requestUrl.contains("ComicDetail")) {
val comic = mangaDetailsParse(response)
return MangasPage(listOf(comic), hasNextPage = false)
}
if (requestUrl.contains("ClassPage")) {
val result = response.parseAs<List<BilibiliComicDto>>()
if (result.code != 0) {
return MangasPage(emptyList(), hasNextPage = false)
}
val comicList = result.data!!.map(::searchMangaFromObject)
val hasNextPage = comicList.size == POPULAR_PER_PAGE
return MangasPage(comicList, hasNextPage)
}
val result = response.parseAs<BilibiliSearchDto>()
if (result.code != 0) {
return MangasPage(emptyList(), hasNextPage = false)
}
val comicList = result.data!!.list.map(::searchMangaFromObject)
val hasNextPage = comicList.size == SEARCH_PER_PAGE
return MangasPage(comicList, hasNextPage)
}
private fun searchMangaFromObject(comic: BilibiliComicDto): SManga = SManga.create().apply {
title = Jsoup.parse(comic.title).text()
thumbnail_url = comic.verticalCover + THUMBNAIL_RESOLUTION
val comicId = if (comic.id == 0) comic.seasonId else comic.id
url = "/detail/mc$comicId"
}
override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url
override fun mangaDetailsRequest(manga: SManga): Request {
val comicId = manga.url.substringAfterLast("/mc").toInt()
val jsonPayload = buildJsonObject { put("comic_id", comicId) }
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.set("Referer", baseUrl + manga.url)
.build()
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/ComicDetail".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val comic = response.parseAs<BilibiliComicDto>().data!!
title = comic.title
author = comic.authorName.joinToString()
genre = comic.styles.joinToString()
status = when {
comic.isFinish == 1 -> SManga.COMPLETED
comic.isOnHiatus -> SManga.ON_HIATUS
else -> SManga.ONGOING
}
description = buildString {
if (comic.hasPaidChapters && !signedIn) {
append("${intl.hasPaidChaptersWarning(comic.paidChaptersCount)}\n\n")
}
append(comic.classicLines)
if (comic.updateWeekdays.isNotEmpty() && status == SManga.ONGOING) {
append("\n\n${intl.informationTitle}:")
append("\n${intl.getUpdateDays(comic.updateWeekdays)}")
}
}
thumbnail_url = comic.verticalCover
url = "/detail/mc" + comic.id
}
// Chapters are available in the same url of the manga details.
override fun chapterListRequest(manga: SManga): Request = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<BilibiliComicDto>()
if (result.code != 0) {
return emptyList()
}
return result.data!!.episodeList.map { ep -> chapterFromObject(ep, result.data.id) }
}
protected open fun chapterFromObject(
episode: BilibiliEpisodeDto,
comicId: Int,
isUnlocked: Boolean = false,
): SChapter = SChapter.create().apply {
name = buildString {
if (episode.isPaid && !isUnlocked) {
append("$EMOJI_LOCKED ")
}
append(episode.shortTitle)
if (episode.title.isNotBlank()) {
append(" - ${episode.title}")
}
}
date_upload = episode.publicationTime.toDate()
url = "/mc$comicId/${episode.id}"
}
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
override fun pageListRequest(chapter: SChapter): Request = imageIndexRequest(chapter.url, "")
override fun pageListParse(response: Response): List<Page> = imageIndexParse(response)
@Suppress("SameParameterValue")
protected open fun imageIndexRequest(chapterUrl: String, credential: String): Request {
val chapterId = chapterUrl.substringAfterLast("/").toInt()
val jsonPayload = buildJsonObject {
put("credential", credential)
put("ep_id", chapterId)
}
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val newHeaders = headersBuilder()
.set("Referer", baseUrl + chapterUrl)
.build()
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/GetImageIndex".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, newHeaders, requestBody)
}
protected open fun imageIndexParse(response: Response): List<Page> {
val result = response.parseAs<BilibiliReader>()
if (result.code != 0) {
return emptyList()
}
val imageQuality = preferences.chapterImageQuality
val imageFormat = preferences.chapterImageFormat
val imageUrls = result.data!!.images.map { it.url(imageQuality, imageFormat) }
val imageTokenRequest = imageTokenRequest(imageUrls)
val imageTokenResponse = client.newCall(imageTokenRequest).execute()
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
return imageTokenResult.data!!.zip(imageUrls).mapIndexed { i, pair ->
Page(i, pair.second, pair.first.imageUrl)
}
}
protected open fun imageTokenRequest(urls: List<String>): Request {
val jsonPayload = buildJsonObject {
put("urls", json.encodeToString(urls))
}
val requestBody = jsonPayload.toString().toRequestBody(JSON_MEDIA_TYPE)
val apiUrl = "$baseUrl/$API_COMIC_V1_COMIC_ENDPOINT/ImageToken".toHttpUrl()
.newBuilder()
.addCommonParameters()
.toString()
return POST(apiUrl, headers, requestBody)
}
override fun imageUrlParse(response: Response): String = ""
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val imageQualityPref = ListPreference(screen.context).apply {
key = "${IMAGE_QUALITY_PREF_KEY}_$lang"
title = intl.imageQualityPrefTitle
entries = intl.imageQualityPrefEntries
entryValues = IMAGE_QUALITY_PREF_ENTRY_VALUES
setDefaultValue(IMAGE_QUALITY_PREF_DEFAULT_VALUE)
summary = "%s"
}
val imageFormatPref = ListPreference(screen.context).apply {
key = "${IMAGE_FORMAT_PREF_KEY}_$lang"
title = intl.imageFormatPrefTitle
entries = IMAGE_FORMAT_PREF_ENTRIES
entryValues = IMAGE_FORMAT_PREF_ENTRY_VALUES
setDefaultValue(IMAGE_FORMAT_PREF_DEFAULT_VALUE)
summary = "%s"
}
screen.addPreference(imageQualityPref)
screen.addPreference(imageFormatPref)
}
abstract fun getAllGenres(): Array<BilibiliTag>
protected open fun getAllAreas(): Array<BilibiliTag> = emptyArray()
protected open fun getAllSortOptions(): Array<BilibiliTag> = arrayOf(
BilibiliTag(intl.sortInterest, 0),
BilibiliTag(intl.sortUpdated, 4),
)
protected open fun getAllStatus(): Array<String> =
arrayOf(intl.statusAll, intl.statusOngoing, intl.statusComplete)
protected open fun getAllPrices(): Array<String> = emptyArray()
override fun getFilterList(): FilterList {
val allAreas = getAllAreas()
val allPrices = getAllPrices()
val filters = listOfNotNull(
StatusFilter(intl.statusLabel, getAllStatus()),
SortFilter(intl.sortLabel, getAllSortOptions(), defaultPopularSort),
PriceFilter(intl.priceLabel, getAllPrices()).takeIf { allPrices.isNotEmpty() },
GenreFilter(intl.genreLabel, getAllGenres()),
AreaFilter(intl.areaLabel, allAreas).takeIf { allAreas.isNotEmpty() },
)
return FilterList(filters)
}
override fun imageRequest(page: Page): Request {
return super.imageRequest(page).newBuilder().tag(TAG_IMAGE_REQUEST)
.tag(TagImagePath::class.java, TagImagePath(page.url)).build()
}
private fun decryptImageIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
if (response.isSuccessful && request.tag() == TAG_IMAGE_REQUEST) {
if (response.body.contentType()?.type == "image") {
return response
}
val cpx = request.url.queryParameter("cpx")
val iv = Base64.decode(cpx, Base64.DEFAULT).copyOfRange(60, 76)
val allBytes = response.body.bytes()
val size =
ByteBuffer.wrap(allBytes.copyOfRange(1, 5)).order(ByteOrder.BIG_ENDIAN).getInt()
val data = allBytes.copyOfRange(5, 5 + size)
val key = allBytes.copyOfRange(5 + size, allBytes.size)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), ivSpec)
val encryptedSize = 20 * 1024 + 16
val decryptedSegment = cipher.doFinal(data, 0, encryptedSize.coerceAtMost(data.size))
val decryptedData = if (encryptedSize < data.size) {
// append remaining data
decryptedSegment + data.copyOfRange(encryptedSize, data.size)
} else {
decryptedSegment
}
val imageExtension = request.url.encodedPath.substringAfterLast(".", "jpg")
return response.newBuilder()
.body(decryptedData.toResponseBody("image/$imageExtension".toMediaType())).build()
}
return response
}
private fun expiredImageTokenIntercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// Get a new image token if the current one expired.
if (response.code == 400 && request.tag() == TAG_IMAGE_REQUEST) {
val imagePath = request.tag(TagImagePath::class)
if (imagePath?.path.isNullOrEmpty()) {
return response
}
response.close()
val imageTokenRequest = imageTokenRequest(listOf(imagePath!!.path))
val imageTokenResponse = chain.proceed(imageTokenRequest)
val imageTokenResult = imageTokenResponse.parseAs<List<BilibiliPageDto>>()
imageTokenResponse.close()
val newPage = imageTokenResult.data!!.first()
val newPageUrl = newPage.imageUrl
val newRequest = imageRequest(Page(0, imagePath.path, newPageUrl))
return chain.proceed(newRequest)
}
return response
}
private val SharedPreferences.chapterImageQuality
get() = when (
getString(
"${IMAGE_QUALITY_PREF_KEY}_$lang",
IMAGE_QUALITY_PREF_DEFAULT_VALUE,
)!!
) {
"hd" -> "1600w"
"sd" -> "1000w"
"low" -> "800w_50q"
else -> "raw"
}
private val SharedPreferences.chapterImageFormat
get() = getString("${IMAGE_FORMAT_PREF_KEY}_$lang", IMAGE_FORMAT_PREF_DEFAULT_VALUE)!!
private inline fun <reified R> List<*>.firstInstanceOrNull(): R? = firstOrNull { it is R } as? R
protected open fun HttpUrl.Builder.addCommonParameters(): HttpUrl.Builder = apply {
if (name == "BILIBILI COMICS") {
addQueryParameter("lang", apiLang)
addQueryParameter("sys_lang", apiLang)
}
addQueryParameter("device", "pc")
addQueryParameter("platform", "web")
}
protected inline fun <reified T> Response.parseAs(): BilibiliResultDto<T> = use {
json.decodeFromString(it.body.string())
}
private fun String.toDate(): Long {
return runCatching { DATE_FORMATTER.parse(this)?.time }
.getOrNull() ?: 0L
}
private class TagImagePath(val path: String)
companion object {
const val CDN_URL = "https://manga.hdslb.com"
const val MODIFIED_CDN_URL = "https://mangaup.hdslb.com"
const val COVER_CDN_URL = "https://i0.hdslb.com"
const val API_COMIC_V1_COMIC_ENDPOINT = "twirp/comic.v1.Comic"
private const val ACCEPT_JSON = "application/json, text/plain, */*"
private const val TAG_IMAGE_REQUEST = "tag_image_request"
val JSON_MEDIA_TYPE = "application/json;charset=UTF-8".toMediaType()
private const val POPULAR_PER_PAGE = 18
private const val SEARCH_PER_PAGE = 9
const val PREFIX_ID_SEARCH = "id:"
private val ID_SEARCH_PATTERN = "^${PREFIX_ID_SEARCH}mc(\\d+)$".toRegex()
private const val IMAGE_QUALITY_PREF_KEY = "chapterImageQuality"
private val IMAGE_QUALITY_PREF_ENTRY_VALUES = arrayOf("raw", "hd", "sd", "low")
private val IMAGE_QUALITY_PREF_DEFAULT_VALUE = IMAGE_QUALITY_PREF_ENTRY_VALUES[1]
private const val IMAGE_FORMAT_PREF_KEY = "chapterImageFormat"
private val IMAGE_FORMAT_PREF_ENTRIES = arrayOf("JPG", "WEBP", "PNG")
private val IMAGE_FORMAT_PREF_ENTRY_VALUES = arrayOf("jpg", "webp", "png")
private val IMAGE_FORMAT_PREF_DEFAULT_VALUE = IMAGE_FORMAT_PREF_ENTRY_VALUES[0]
const val THUMBNAIL_RESOLUTION = "@512w.jpg"
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
}
private const val EMOJI_LOCKED = "\uD83D\uDD12"
const val EMOJI_WARNING = "\u26A0\uFE0F"
}
}

View File

@ -1,117 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class BilibiliResultDto<T>(
val code: Int = 0,
val data: T? = null,
@SerialName("msg") val message: String = "",
)
@Serializable
data class BilibiliSearchDto(
val list: List<BilibiliComicDto> = emptyList(),
)
@Serializable
data class BilibiliComicDto(
@SerialName("author_name") val authorName: List<String> = emptyList(),
@SerialName("classic_lines") val classicLines: String = "",
@SerialName("comic_id") val comicId: Int = 0,
@SerialName("ep_list") val episodeList: List<BilibiliEpisodeDto> = emptyList(),
val id: Int = 0,
@SerialName("is_finish") val isFinish: Int = 0,
@SerialName("temp_stop_update") val isOnHiatus: Boolean = false,
@SerialName("season_id") val seasonId: Int = 0,
val styles: List<String> = emptyList(),
val title: String,
@SerialName("update_weekday") val updateWeekdays: List<Int> = emptyList(),
@SerialName("vertical_cover") val verticalCover: String = "",
) {
val hasPaidChapters: Boolean
get() = paidChaptersCount > 0
val paidChaptersCount: Int
get() = episodeList.filter { it.isPaid }.size
}
@Serializable
data class BilibiliEpisodeDto(
val id: Int,
@SerialName("is_in_free") val isInFree: Boolean,
@SerialName("is_locked") val isLocked: Boolean,
@SerialName("pay_gold") val payGold: Int,
@SerialName("pay_mode") val payMode: Int,
@SerialName("pub_time") val publicationTime: String,
@SerialName("short_title") val shortTitle: String,
val title: String,
) {
val isPaid = payMode == 1 && payGold > 0
}
@Serializable
data class BilibiliReader(
val images: List<BilibiliImageDto> = emptyList(),
)
@Serializable
data class BilibiliImageDto(
val path: String,
@SerialName("x") val width: Int,
@SerialName("y") val height: Int,
) {
fun url(quality: String, format: String): String {
val imageWidth = if (quality == "raw") "${width}w" else quality
return "$path@$imageWidth.$format"
}
}
@Serializable
data class BilibiliPageDto(
val token: String,
val url: String,
@SerialName("complete_url")
val completeUrl: String,
) {
val imageUrl: String
get() = completeUrl.ifEmpty { "$url?token=$token" }
}
@Serializable
data class BilibiliAccessTokenCookie(
val accessToken: String,
val refreshToken: String,
val area: String,
)
@Serializable
data class BilibiliAccessToken(
@SerialName("access_token") val accessToken: String,
@SerialName("refresh_token") val refreshToken: String,
)
@Serializable
data class BilibiliUserEpisodes(
@SerialName("unlocked_eps") val unlockedEpisodes: List<BilibiliUnlockedEpisode>? = emptyList(),
)
@Serializable
data class BilibiliUnlockedEpisode(
@SerialName("ep_id") val id: Int = 0,
)
@Serializable
data class BilibiliGetCredential(
@SerialName("comic_id") val comicId: Int,
@SerialName("ep_id") val episodeId: Int,
val type: Int,
)
@Serializable
data class BilibiliCredential(
val credential: String,
)

View File

@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
import eu.kanade.tachiyomi.source.model.Filter
data class BilibiliTag(val name: String, val id: Int) {
override fun toString(): String = name
}
open class EnhancedSelect<T>(name: String, values: Array<T>, state: Int = 0) :
Filter.Select<T>(name, values, state) {
val selected: T?
get() = values.getOrNull(state)
}
class GenreFilter(label: String, genres: Array<BilibiliTag>) :
EnhancedSelect<BilibiliTag>(label, genres)
class AreaFilter(label: String, genres: Array<BilibiliTag>) :
EnhancedSelect<BilibiliTag>(label, genres)
class SortFilter(label: String, options: Array<BilibiliTag>, state: Int = 0) :
EnhancedSelect<BilibiliTag>(label, options, state)
class StatusFilter(label: String, statuses: Array<String>) :
Filter.Select<String>(label, statuses)
class PriceFilter(label: String, prices: Array<String>) :
Filter.Select<String>(label, prices)

View File

@ -1,226 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
import java.text.DateFormatSymbols
import java.text.NumberFormat
import java.util.Locale
class BilibiliIntl(private val lang: String) {
private val locale by lazy { Locale.forLanguageTag(lang) }
private val dateFormatSymbols by lazy { DateFormatSymbols(locale) }
private val numberFormat by lazy { NumberFormat.getInstance(locale) }
val statusLabel: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "进度"
SPANISH -> "Estado"
else -> "Status"
}
val sortLabel: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "排序"
INDONESIAN -> "Urutkan dengan"
SPANISH -> "Ordenar por"
else -> "Sort by"
}
val genreLabel: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "题材"
SPANISH -> "Género"
else -> "Genre"
}
val areaLabel: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "地区"
else -> "Area"
}
val priceLabel: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "收费"
INDONESIAN -> "Harga"
SPANISH -> "Precio"
else -> "Price"
}
fun hasPaidChaptersWarning(chapterCount: Int): String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE ->
"${Bilibili.EMOJI_WARNING} 此漫画有 ${chapterCount.localized} 个付费章节,已在目录中隐藏。" +
"如果你已购买,请在 WebView 登录并刷新目录,即可阅读已购章节。"
SPANISH ->
"${Bilibili.EMOJI_WARNING} ADVERTENCIA: Esta serie tiene ${chapterCount.localized} " +
"capítulos pagos que fueron filtrados de la lista de capítulos. Si ya has " +
"desbloqueado y tiene alguno en su cuenta, inicie sesión en WebView y " +
"actualice la lista de capítulos para leerlos."
else ->
"${Bilibili.EMOJI_WARNING} WARNING: This series has ${chapterCount.localized} paid " +
"chapters. If you have any unlocked in your account then sign in through WebView " +
"to be able to read them."
}
val imageQualityPrefTitle: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "章节图片质量"
INDONESIAN -> "Kualitas gambar"
SPANISH -> "Calidad de imagen del capítulo"
else -> "Chapter image quality"
}
val imageQualityPrefEntries: Array<String> = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> arrayOf("原图", "高清 (1600w)", "标清 (1000w)", "低清 (800w)")
else -> arrayOf("Raw", "HD (1600w)", "SD (1000w)", "Low (800w)")
}
val imageFormatPrefTitle: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "章节图片格式"
INDONESIAN -> "Format gambar"
SPANISH -> "Formato de la imagen del capítulo"
else -> "Chapter image format"
}
val sortInterest: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "为你推荐"
INDONESIAN -> "Kamu Mungkin Suka"
SPANISH -> "Sugerencia"
else -> "Interest"
}
@Suppress("UNUSED") // In BilibiliManga
val sortPopular: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "人气推荐"
INDONESIAN -> "Populer"
SPANISH -> "Popularidad"
FRENCH -> "Préférences"
else -> "Popular"
}
val sortUpdated: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "更新时间"
INDONESIAN -> "Terbaru"
SPANISH -> "Actualización"
FRENCH -> "Récent"
else -> "Updated"
}
@Suppress("UNUSED") // In BilibiliManga
val sortAdded: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "上架时间"
else -> "Added"
}
@Suppress("UNUSED") // In BilibiliManga
val sortFollowers: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "追漫人数"
else -> "Followers count"
}
val statusAll: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "全部"
INDONESIAN -> "Semua"
SPANISH -> "Todos"
FRENCH -> "Tout"
else -> "All"
}
val statusOngoing: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "连载中"
INDONESIAN -> "Berlangsung"
SPANISH -> "En curso"
FRENCH -> "En cours"
else -> "Ongoing"
}
val statusComplete: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "已完结"
INDONESIAN -> "Tamat"
SPANISH -> "Finalizado"
FRENCH -> "Complet"
else -> "Completed"
}
@Suppress("UNUSED") // In BilibiliManga
val priceAll: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "全部"
INDONESIAN -> "Semua"
SPANISH -> "Todos"
else -> "All"
}
@Suppress("UNUSED") // In BilibiliManga
val priceFree: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "免费"
INDONESIAN -> "Bebas"
SPANISH -> "Gratis"
else -> "Free"
}
@Suppress("UNUSED") // In BilibiliManga
val pricePaid: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "付费"
INDONESIAN -> "Dibayar"
SPANISH -> "Pago"
else -> "Paid"
}
@Suppress("UNUSED") // In BilibiliManga
val priceWaitForFree: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "等就免费"
else -> "Wait for free"
}
@Suppress("UNUSED") // In BilibiliComics
val failedToRefreshToken: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "无法刷新令牌。请打开 WebView 修正错误。"
SPANISH -> "Error al actualizar el token. Abra el WebView para solucionar este error."
else -> "Failed to refresh the token. Open the WebView to fix this error."
}
@Suppress("UNUSED") // In BilibiliComics
val failedToGetCredential: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "无法获取阅读章节所需的凭证。"
SPANISH -> "Erro al obtener la credencial para leer el capítulo."
else -> "Failed to get the credential to read the chapter."
}
val informationTitle: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "信息"
SPANISH -> "Información"
else -> "Information"
}
private val updatesDaily: String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "每日更新"
SPANISH -> "Actualizaciones diarias"
else -> "Updates daily"
}
private fun updatesEvery(days: String): String = when (lang) {
CHINESE, SIMPLIFIED_CHINESE -> "${days}更新"
SPANISH -> "Actualizaciones todos los $days"
else -> "Updates every $days"
}
fun getUpdateDays(dayIndexes: List<Int>): String {
val shortWeekDays = dateFormatSymbols.shortWeekdays.filterNot(String::isBlank)
if (dayIndexes.size == shortWeekDays.size) return updatesDaily
val shortWeekDaysUpperCased = shortWeekDays.map {
it.replaceFirstChar { char -> char.uppercase(locale) }
}
val days = dayIndexes.joinToString { shortWeekDaysUpperCased[it] }
return updatesEvery(days)
}
private val Int.localized: String
get() = numberFormat.format(this)
companion object {
const val CHINESE = "zh"
const val INDONESIAN = "id"
const val SIMPLIFIED_CHINESE = "zh-Hans"
const val SPANISH = "es"
const val FRENCH = "fr"
@Suppress("UNUSED") // In BilibiliComics
const val ENGLISH = "en"
}
}

View File

@ -1,85 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
import eu.kanade.tachiyomi.source.model.SChapter
import okhttp3.Headers
import okhttp3.Response
class BilibiliManga : Bilibili(
"哔哩哔哩漫画",
"https://manga.bilibili.com",
BilibiliIntl.SIMPLIFIED_CHINESE,
) {
override val id: Long = 3561131545129718586
override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", DEFAULT_USER_AGENT)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<BilibiliComicDto>()
if (result.code != 0) {
return emptyList()
}
val data = result.data!!
val id = data.id
return data.episodeList.mapNotNull { episode ->
if (episode.isInFree || !episode.isLocked) {
chapterFromObject(episode, id)
} else {
null
}
}
}
override val defaultPopularSort: Int = 0
override val defaultLatestSort: Int = 1
override fun getAllSortOptions(): Array<BilibiliTag> = arrayOf(
BilibiliTag(intl.sortPopular, 0),
BilibiliTag(intl.sortUpdated, 1),
BilibiliTag(intl.sortFollowers, 2),
BilibiliTag(intl.sortAdded, 3),
)
override fun getAllPrices(): Array<String> =
arrayOf(intl.priceAll, intl.priceFree, intl.pricePaid, intl.priceWaitForFree)
override fun getAllGenres(): Array<BilibiliTag> = arrayOf(
BilibiliTag("全部", -1),
BilibiliTag("竞技", 1034),
BilibiliTag("冒险", 1013),
BilibiliTag("热血", 999),
BilibiliTag("搞笑", 994),
BilibiliTag("恋爱", 995),
BilibiliTag("少女", 1026),
BilibiliTag("日常", 1020),
BilibiliTag("校园", 1001),
BilibiliTag("治愈", 1007),
BilibiliTag("古风", 997),
BilibiliTag("玄幻", 1016),
BilibiliTag("奇幻", 998),
BilibiliTag("惊奇", 996),
BilibiliTag("悬疑", 1023),
BilibiliTag("都市", 1002),
BilibiliTag("剧情", 1030),
BilibiliTag("总裁", 1004),
BilibiliTag("科幻", 1015),
BilibiliTag("正能量", 1028),
)
override fun getAllAreas(): Array<BilibiliTag> = arrayOf(
BilibiliTag("全部", -1),
BilibiliTag("大陆", 1),
BilibiliTag("日本", 2),
BilibiliTag("韩国", 6),
BilibiliTag("其他", 5),
)
companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
}
}

View File

@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.bilibilimanga
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://www.bilibilicomics.com/detail/xxx intents and redirects them to
* the main tachiyomi process. The idea is to not install the intent filter unless
* you have this extension installed, but still let the main tachiyomi app control
* things.
*
* Main goal was to make it easier to open manga in Tachiyomi in spite of the DDoS blocking
* the usual search screen from working.
*/
class BilibiliUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
// Mobile site of https://manga.bilibili.com starts with path "m"
val titleId = if (pathSegments[0] == "m") pathSegments[2] else pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", Bilibili.PREFIX_ID_SEARCH + titleId)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("BilibiliUrlActivity", e.toString())
}
} else {
Log.e("BilibiliUrlActivity", "Could not parse URI from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".zh.kuaikanmanhua.KuaikanmanhuaUrlActivity"
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="*.kuaikanmanhua.com" />
<data android:host="kuaikanmanhua.com" />
<data
android:host="m.kuaikanmanhua.com"
android:pathPattern="/mobile/..*"
android:scheme="https" />
<data
android:pathPattern="/web/topic/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,7 +0,0 @@
ext {
extName = 'Kuaikanmanhua'
extClass = '.Kuaikanmanhua'
extVersionCode = 10
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,285 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.kuaikanmanhua
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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 eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
class Kuaikanmanhua : HttpSource() {
override val name = "快看漫画"
override val id: Long = 8099870292642776005
override val baseUrl = "https://www.kuaikanmanhua.com"
override val lang = "zh"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val apiUrl = "https://api.kkmh.com"
private val json: Json by injectLazy()
// Popular
override fun popularMangaRequest(page: Int): Request {
return GET("$apiUrl/v1/topic_new/lists/get_by_tag?tag=0&since=${(page - 1) * 10}", headers)
}
override fun popularMangaParse(response: Response): MangasPage {
val body = response.body.string()
val jsonList = json.parseToJsonElement(body).jsonObject["data"]!!
.jsonObject["topics"]!!
.jsonArray
return parseMangaJsonArray(jsonList)
}
private fun parseMangaJsonArray(jsonList: JsonArray, isSearch: Boolean = false): MangasPage {
val mangaList = jsonList.map {
val mangaObj = it.jsonObject
SManga.create().apply {
title = mangaObj["title"]!!.jsonPrimitive.content
thumbnail_url = mangaObj["vertical_image_url"]!!.jsonPrimitive.content
url = "/web/topic/" + mangaObj["id"]!!.jsonPrimitive.int
}
}
// KKMH does not have pages when you search
return MangasPage(mangaList, hasNextPage = mangaList.size > 9 && !isSearch)
}
// Latest
override fun latestUpdatesRequest(page: Int): Request {
return GET("$apiUrl/v1/topic_new/lists/get_by_tag?tag=19&since=${(page - 1) * 10}", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(TOPIC_ID_SEARCH_PREFIX)) {
val newQuery = query.removePrefix(TOPIC_ID_SEARCH_PREFIX)
return client.newCall(GET("$apiUrl/v1/topics/$newQuery"))
.asObservableSuccess()
.map { response ->
val details = mangaDetailsParse(response)
details.url = "/web/topic/$newQuery"
MangasPage(listOf(details), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotEmpty()) {
GET("$apiUrl/v1/search/topic?q=$query&size=18", headers)
} else {
lateinit var genre: String
lateinit var status: String
filters.forEach { filter ->
when (filter) {
is GenreFilter -> {
genre = filter.toUriPart()
}
is StatusFilter -> {
status = filter.toUriPart()
}
else -> {}
}
}
GET("$apiUrl/v1/search/by_tag?since=${(page - 1) * 10}&tag=$genre&sort=1&query_category=%7B%22update_status%22:$status%7D")
}
}
override fun searchMangaParse(response: Response): MangasPage {
val body = response.body.string()
val jsonObj = json.parseToJsonElement(body).jsonObject["data"]!!.jsonObject
if (jsonObj["hit"] != null) {
return parseMangaJsonArray(jsonObj["hit"]!!.jsonArray, true)
}
return parseMangaJsonArray(jsonObj["topics"]!!.jsonArray, false)
}
// Details
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
// Convert the stored url to one that works with the api
val newUrl = apiUrl + "/v1/topics/" + manga.url.trimEnd('/').substringAfterLast("/")
val response = client.newCall(GET(newUrl)).execute()
val sManga = mangaDetailsParse(response).apply { initialized = true }
return Observable.just(sManga)
}
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val data = json.parseToJsonElement(response.body.string())
.jsonObject["data"]!!
.jsonObject
title = data["title"]!!.jsonPrimitive.content
thumbnail_url = data["vertical_image_url"]!!.jsonPrimitive.content
author = data["user"]!!.jsonObject["nickname"]!!.jsonPrimitive.content
description = data["description"]!!.jsonPrimitive.content
status = data["update_status_code"]!!.jsonPrimitive.int
}
// Chapters & Pages
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val newUrl = apiUrl + "/v1/topics/" + manga.url.trimEnd('/').substringAfterLast("/")
val response = client.newCall(GET(newUrl)).execute()
val chapters = chapterListParse(response)
return Observable.just(chapters)
}
override fun chapterListParse(response: Response): List<SChapter> {
val data = json.parseToJsonElement(response.body.string())
.jsonObject["data"]!!
.jsonObject
val chaptersJson = data["comics"]!!.jsonArray
val chapters = mutableListOf<SChapter>()
for (i in 0 until chaptersJson.size) {
val obj = chaptersJson[i].jsonObject
chapters.add(
SChapter.create().apply {
url = "/web/comic/" + obj["id"]!!.jsonPrimitive.content
name = obj["title"]!!.jsonPrimitive.content +
if (!obj["can_view"]!!.jsonPrimitive.boolean) {
" \uD83D\uDD12"
} else {
""
}
date_upload = obj["created_at"]!!.jsonPrimitive.long * 1000
},
)
}
return chapters
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val request = client.newCall(pageListRequest(chapter)).execute()
return Observable.just(pageListParse(request))
}
override fun pageListRequest(chapter: SChapter): Request {
// if (chapter.name.endsWith("🔒")) {
// throw Exception("[此章节为付费内容]")
// }
return GET(baseUrl + chapter.url)
}
private val fixJson: (MatchResult) -> CharSequence = {
match: MatchResult ->
val str = match.value
val out = str[0] + "\"" + str.subSequence(1, str.length - 1) + "\"" + str[str.length - 1]
out
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val script = document.selectFirst("script:containsData(comicImages)")!!.data()
val images = script.substringAfter("comicImages:")
.substringBefore(",is_vip_exclusive")
.replace("""(:([^\[\{\"]+?)[\},])""".toRegex(), fixJson)
.replace("""([,{]([^\[\{\"]+?)[\}:])""".toRegex(), fixJson)
.let { json.parseToJsonElement(it).jsonArray }
val variable = script.substringAfter("(function(")
.substringBefore("){")
.split(",")
val value = script.substringAfterLast("}}(")
.substringBefore("));")
.split(",")
return images.mapIndexed { index, jsonEl ->
val urlVar = jsonEl.jsonObject["url"]!!.jsonPrimitive.content
val imageUrl = value[variable.indexOf(urlVar)]
.replace("\\u002F", "/")
.replace("\"", "")
Page(index, "", imageUrl)
}
}
// Filters
override fun getFilterList() = FilterList(
Filter.Header("注意:不影響按標題搜索"),
StatusFilter(),
GenreFilter(),
)
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
private class GenreFilter : UriPartFilter(
"题材",
arrayOf(
Pair("全部", "0"),
Pair("恋爱", "20"),
Pair("古风", "46"),
Pair("校园", "47"),
Pair("奇幻", "22"),
Pair("大女主", "77"),
Pair("治愈", "27"),
Pair("总裁", "52"),
Pair("完结", "40"),
Pair("唯美", "58"),
Pair("日漫", "57"),
Pair("韩漫", "60"),
Pair("穿越", "80"),
Pair("正能量", "54"),
Pair("灵异", "32"),
Pair("爆笑", "24"),
Pair("都市", "48"),
Pair("萌系", "62"),
Pair("玄幻", "63"),
Pair("日常", "19"),
Pair("投稿", "76"),
),
)
private class StatusFilter : UriPartFilter(
"类别",
arrayOf(
Pair("全部", "1"),
Pair("连载中", "2"),
Pair("已完结", "3"),
),
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
companion object {
const val TOPIC_ID_SEARCH_PREFIX = "topic:"
}
}

View File

@ -1,38 +0,0 @@
package eu.kanade.tachiyomi.extension.zh.kuaikanmanhua
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 KuaikanmanhuaUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val host = intent?.data?.host
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 1) {
val id = when (host) {
"m.kuaikanmanhua.com" -> pathSegments[1]
else -> pathSegments[2]
}
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${Kuaikanmanhua.TOPIC_ID_SEARCH_PREFIX}$id")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("KkmhUrlActivity", e.toString())
}
} else {
Log.e("KkmhUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}