ComiciViewer: add multisrc Comici Viewer (#10970)

* multisrc comici

* fixes and filters

* fix parameters

* fixes and sources
This commit is contained in:
manti 2025-10-11 15:44:18 +02:00 committed by Draff
parent 9282d65dd8
commit ccbde23c1f
Signed by: Draff
GPG Key ID: E8A89F3211677653
79 changed files with 615 additions and 0 deletions

View File

@ -0,0 +1,5 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,244 @@
package eu.kanade.tachiyomi.multisrc.comiciviewer
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
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 keiyoushi.utils.firstInstance
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import java.text.SimpleDateFormat
import java.util.Locale
abstract class ComiciViewer(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ConfigurableSource, HttpSource() {
private val preferences: SharedPreferences by getPreferencesLazy()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.JAPAN)
override val supportsLatest = true
override val client = super.client.newBuilder()
.addInterceptor(ImageInterceptor())
.build()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/ranking/manga", headers)
override fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select("div.ranking-box-vertical, div.ranking-box-vertical-top3").map { element ->
SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
title = element.selectFirst(".title-text")!!.text()
thumbnail_url = element.selectFirst("source")?.attr("data-srcset")?.substringBefore(" ")?.let { "https:$it" }
}
}
return MangasPage(mangas, false)
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/category/manga", headers)
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select("div.category-box-vertical").map { element ->
SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
title = element.selectFirst(".title-text")!!.text()
thumbnail_url = element.selectFirst("source")?.attr("data-srcset")?.substringBefore(" ")?.let { "https:$it" }
}
}
return MangasPage(mangas, false)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
val url = "$baseUrl/search".toHttpUrl().newBuilder()
.addQueryParameter("keyword", query)
.addQueryParameter("page", (page - 1).toString())
.addQueryParameter("filter", "series")
.build()
return GET(url, headers)
}
val filterList = if (filters.isEmpty()) getFilterList() else filters
val browseFilter = filterList.firstInstance<BrowseFilter>()
val pathAndQuery = getFilterOptions()[browseFilter.state].second
val url = (baseUrl + pathAndQuery).toHttpUrl().newBuilder().build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val url = response.request.url.toString()
return when {
url.contains("/ranking/") -> popularMangaParse(response)
url.contains("/category/") -> latestUpdatesParse(response)
else -> {
val document = response.asJsoup()
val mangas = document.select("div.manga-store-item").map { element ->
SManga.create().apply {
setUrlWithoutDomain(
element.selectFirst("a.c-ms-clk-article")!!.attr("href"),
)
title = element.selectFirst("h2.manga-title")!!.text()
thumbnail_url =
element.selectFirst("source")?.attr("data-srcset")?.substringBefore(" ")
?.let { "https:$it" }
}
}
val hasNextPage = document.selectFirst("li.mode-paging-active + li > a") != null
return MangasPage(mangas, hasNextPage)
}
}
}
override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup()
return SManga.create().apply {
title = document.select("h1.series-h-title span").last()!!.text()
author = document.select("div.series-h-credit-user").text()
artist = author
description = document.selectFirst("div.series-h-credit-info-text-text")?.text()
genre = document.select("a.series-h-tag-link").joinToString { it.text().removePrefix("#") }
thumbnail_url = document.selectFirst("div.series-h-img source")?.attr("data-srcset")?.substringBefore(" ")?.let { "https:$it" }
}
}
override fun chapterListRequest(manga: SManga): Request {
return GET(baseUrl + manga.url + "/list?s=1", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val showLocked = preferences.getBoolean(SHOW_LOCKED_PREF_KEY, true)
val document = response.asJsoup()
return document.select("div.series-ep-list-item").mapNotNull { element ->
val link = element.selectFirst("a.g-episode-link-wrapper")!!
val isFree = element.selectFirst("span.free-icon-new") != null
val isTicketLocked = element.selectFirst("img[data-src*='free_charge_ja.svg']") != null
val isCoinLocked = element.selectFirst("img[data-src*='coin.svg']") != null
val isLocked = !isFree
if (!showLocked && isLocked) {
return@mapNotNull null
}
SChapter.create().apply {
val chapterUrl = link.attr("data-href")
if (chapterUrl.isNotEmpty()) {
setUrlWithoutDomain(chapterUrl)
} else {
url = response.request.url.toString() + "#" + link.attr("data-article") + DUMMY_URL_SUFFIX
}
name = link.selectFirst("span.series-ep-list-item-h-text")!!.text()
when {
isTicketLocked -> name = "🔒 $name"
isCoinLocked -> name = "\uD83E\uDE99 $name"
}
date_upload = dateFormat.tryParse(element.selectFirst("time")?.attr("datetime"))
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.endsWith(DUMMY_URL_SUFFIX)) {
throw Exception("Log in via WebView to read purchased chapters and refresh the entry")
}
return super.pageListRequest(chapter)
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val viewer = document.selectFirst("#comici-viewer") ?: throw Exception("You need to log in via WebView to read this chapter or purchase this chapter")
val comiciViewerId = viewer.attr("comici-viewer-id")
val memberJwt = viewer.attr("data-member-jwt")
val requestUrl = "$baseUrl/book/contentsInfo".toHttpUrl().newBuilder()
.addQueryParameter("comici-viewer-id", comiciViewerId)
.addQueryParameter("user-id", memberJwt)
.addQueryParameter("page-from", "0")
val pageTo = client.newCall(GET(requestUrl.addQueryParameter("page-to", "1").build(), headers))
.execute().use { initialResponse ->
if (!initialResponse.isSuccessful) {
throw Exception("Failed to get page list")
}
initialResponse.parseAs<ViewerResponse>().totalPages.toString()
}
val getAllPagesUrl = requestUrl.setQueryParameter("page-to", pageTo).build()
return client.newCall(GET(getAllPagesUrl, headers)).execute().use { allPagesResponse ->
if (allPagesResponse.isSuccessful) {
allPagesResponse.parseAs<ViewerResponse>().result.map { resultItem ->
val urlBuilder = resultItem.imageUrl.toHttpUrl().newBuilder()
if (resultItem.scramble.isNotEmpty()) {
urlBuilder.addQueryParameter("scramble", resultItem.scramble)
}
Page(
index = resultItem.sort,
imageUrl = urlBuilder.build().toString(),
)
}.sortedBy { it.index }
} else {
throw Exception("Failed to get full page list")
}
}
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_LOCKED_PREF_KEY
title = "Show locked chapters"
setDefaultValue(true)
}.also(screen::addPreference)
}
protected open class BrowseFilter(vals: Array<String>) : Filter.Select<String>("Filter by", vals)
protected open fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("ランキング", "/ranking/manga"),
Pair("読み切り", "/category/manga?type=読み切り"),
Pair("完結", "/category/manga?type=完結"),
Pair("月曜日", "/category/manga?type=連載中&day=月"),
Pair("火曜日", "/category/manga?type=連載中&day=火"),
Pair("水曜日", "/category/manga?type=連載中&day=水"),
Pair("木曜日", "/category/manga?type=連載中&day=木"),
Pair("金曜日", "/category/manga?type=連載中&day=金"),
Pair("土曜日", "/category/manga?type=連載中&day=土"),
Pair("日曜日", "/category/manga?type=連載中&day=日"),
Pair("その他", "/category/manga?type=連載中&day=その他"),
)
override fun getFilterList() = FilterList(
BrowseFilter(getFilterOptions().map { it.first }.toTypedArray()),
)
// Unsupported
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
companion object {
private const val SHOW_LOCKED_PREF_KEY = "pref_show_locked_chapters"
private const val DUMMY_URL_SUFFIX = "NeedLogin"
}
}

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.multisrc.comiciviewer
import kotlinx.serialization.Serializable
@Serializable
class ViewerResponse(
val result: List<PageDto>,
val totalPages: Int,
)
@Serializable
class PageDto(
val imageUrl: String,
val scramble: String,
val sort: Int,
)
@Serializable
class TilePos(
val x: Int,
val y: Int,
)

View File

@ -0,0 +1,86 @@
package eu.kanade.tachiyomi.multisrc.comiciviewer
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
class ImageInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val scrambleData = request.url.queryParameter("scramble")
if (scrambleData.isNullOrEmpty()) {
return chain.proceed(request)
}
val newUrl = request.url.newBuilder()
.removeAllQueryParameters("scramble")
.build()
val newRequest = request.newBuilder().url(newUrl).build()
val response = chain.proceed(newRequest)
if (!response.isSuccessful) {
return response
}
val tiles = buildList {
scrambleData.drop(1).dropLast(1).replace(" ", "").split(",").forEach {
val scrambleInt = it.toInt()
add(TilePos(scrambleInt / 4, scrambleInt % 4))
}
}
val scrambledImg = BitmapFactory.decodeStream(response.body.byteStream())
val descrambledImg =
unscrambleImage(scrambledImg, scrambledImg.width, scrambledImg.height, tiles)
val output = ByteArrayOutputStream()
descrambledImg.compress(Bitmap.CompressFormat.JPEG, 90, output)
val body = output.toByteArray().toResponseBody("image/jpeg".toMediaType())
return response.newBuilder().body(body).build()
}
private fun unscrambleImage(
rawImage: Bitmap,
width: Int,
height: Int,
tiles: List<TilePos>,
): Bitmap {
val descrambledImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(descrambledImg)
val tileWidth = width / 4
val tileHeight = height / 4
var count = 0
for (x in 0..3) {
for (y in 0..3) {
val desRect = Rect(
x * tileWidth,
y * tileHeight,
(x + 1) * tileWidth,
(y + 1) * tileHeight,
)
val srcRect = Rect(
tiles[count].x * tileWidth,
tiles[count].y * tileHeight,
(tiles[count].x + 1) * tileWidth,
(tiles[count].y + 1) * tileHeight,
)
canvas.drawBitmap(rawImage, srcRect, desRect, null)
count++
}
}
return descrambledImg
}
}

View File

@ -0,0 +1,10 @@
ext {
extName = "Big Comics"
extClass = ".BigComics"
themePkg = 'comiciviewer'
baseUrl = "https://bigcomics.jp"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.extension.ja.bigcomics
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class BigComics : ComiciViewer(
"Big Comics",
"https://bigcomics.jp",
"ja",
)

View File

@ -0,0 +1,10 @@
ext {
extName = "Comic MeDu"
extClass = ".ComicMeDu"
themePkg = 'comiciviewer'
baseUrl = "https://comic-medu.com"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.ja.comicmedu
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class ComicMeDu : ComiciViewer(
"Comic MeDu",
"https://comic-medu.com",
"ja",
) {
override fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("ランキング", "/ranking/manga"),
Pair("読み切り", "/category/manga?type=読み切り"),
Pair("完結", "/category/manga?type=完結"),
Pair("連載", "/category/manga?type=連載中"),
)
}

View File

@ -0,0 +1,10 @@
ext {
extName = "Comic Pash"
extClass = ".ComicPash"
themePkg = 'comiciviewer'
baseUrl = "https://comicpash.jp"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.extension.ja.comicpash
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class ComicPash : ComiciViewer(
"Comic Pash",
"https://comicpash.jp",
"ja",
) {
override fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("ランキング", "/ranking/manga"),
Pair("読み切り", "/category/manga?type=読み切り"),
Pair("完結", "/category/manga?type=完結"),
Pair("月曜日", "/category/manga?type=連載中&day=月"),
Pair("火曜日", "/category/manga?type=連載中&day=火"),
Pair("水曜日", "/category/manga?type=連載中&day=水"),
Pair("木曜日", "/category/manga?type=連載中&day=木"),
Pair("金曜日", "/category/manga?type=連載中&day=金"),
Pair("土曜日", "/category/manga?type=連載中&day=土"),
Pair("日曜日", "/category/manga?type=連載中&day=日"),
)
}

View File

@ -0,0 +1,10 @@
ext {
extName = "Comic Ride"
extClass = ".ComicRide"
themePkg = 'comiciviewer'
baseUrl = "https://comicride.jp"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.extension.ja.comicride
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class ComicRide : ComiciViewer(
"Comic Ride",
"https://comicride.jp",
"ja",
)

View File

@ -0,0 +1,10 @@
ext {
extName = "J-N Books"
extClass = ".JNBooks"
themePkg = 'comiciviewer'
baseUrl = "https://comic.j-nbooks.jp"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.extension.ja.jnbooks
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
import eu.kanade.tachiyomi.source.model.MangasPage
import okhttp3.Request
import okhttp3.Response
class JNBooks : ComiciViewer(
"J-N Books",
"https://comic.j-nbooks.jp",
"ja",
) {
override val supportsLatest = false
override fun popularMangaRequest(page: Int): Request = latestUpdatesRequest(page)
override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response)
override fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("読み切り", "/category/manga?type=読み切り"),
Pair("完結", "/category/manga?type=完結"),
Pair("連載", "/category/manga?type=連載中"),
)
}

View File

@ -0,0 +1,10 @@
ext {
extName = "KimiComi"
extClass = ".KimiComi"
themePkg = 'comiciviewer'
baseUrl = "https://kimicomi.com"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.extension.ja.kimicomi
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class KimiComi : ComiciViewer(
"KimiComi",
"https://kimicomi.com",
"ja",
) {
override fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("ランキング", "/ranking/manga"),
Pair("読み切り", "/category/manga?type=読み切り"),
Pair("完結", "/category/manga?type=完結"),
Pair("月曜日", "/category/manga?type=連載中&day=月"),
Pair("火曜日", "/category/manga?type=連載中&day=火"),
Pair("水曜日", "/category/manga?type=連載中&day=水"),
Pair("木曜日", "/category/manga?type=連載中&day=木"),
Pair("金曜日", "/category/manga?type=連載中&day=金"),
Pair("土曜日", "/category/manga?type=連載中&day=土"),
Pair("日曜日", "/category/manga?type=連載中&day=日"),
)
}

View File

@ -0,0 +1,10 @@
ext {
extName = "MagKan"
extClass = ".MagKan"
themePkg = 'comiciviewer'
baseUrl = "https://kansai.mag-garden.co.jp"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.extension.ja.magkan
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
import eu.kanade.tachiyomi.source.model.MangasPage
import okhttp3.Request
import okhttp3.Response
class MagKan : ComiciViewer(
"MagKan",
"https://kansai.mag-garden.co.jp",
"ja",
) {
override val supportsLatest = false
override fun popularMangaRequest(page: Int): Request = latestUpdatesRequest(page)
override fun popularMangaParse(response: Response): MangasPage = latestUpdatesParse(response)
override fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("読み切り", "/category/manga?type=読み切り"),
Pair("連載", "/category/manga?type=連載中"),
)
}

View File

@ -0,0 +1,10 @@
ext {
extName = "MangaBang Comics"
extClass = ".MangaBang"
themePkg = 'comiciviewer'
baseUrl = "https://comics.manga-bang.com"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.ja.mangabang
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class MangaBang : ComiciViewer(
"MangaBang Comics",
"https://comics.manga-bang.com",
"ja",
) {
override fun getFilterOptions(): List<Pair<String, String>> = listOf(
Pair("ランキング", "/ranking/manga"),
Pair("完結", "/category/manga?type=完結"),
Pair("連載", "/category/manga?type=連載中"),
)
}

View File

@ -0,0 +1,10 @@
ext {
extName = "Young Animal"
extClass = ".YoungAnimal"
themePkg = 'comiciviewer'
baseUrl = "https://younganimal.com"
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.extension.ja.younganimal
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class YoungAnimal : ComiciViewer(
"Young Animal",
"https://younganimal.com",
"ja",
)

View File

@ -0,0 +1,10 @@
ext {
extName = "Young Champion"
extClass = ".YoungChampion"
themePkg = 'comiciviewer'
baseUrl = "https://youngchampion.jp"
overrideVersionCode = 0
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.extension.ja.youngchampion
import eu.kanade.tachiyomi.multisrc.comiciviewer.ComiciViewer
class YoungChampion : ComiciViewer(
"Young Champion",
"https://youngchampion.jp",
"ja",
)