Add MangaFire and create MangaReader multisrc (#16138)
* Add MangaFire.to * Fixes Updated 'searchMangaNextPageSelector' Added throwing 'UnsupportedOperationException' for unused selector * Fixed naming of filters and fixed parameter's name of minimal chapters' filter * Fixed language parameter in query * Clean up MangaFire filters * Clean up MangaFire descrambler * Create MangaReader multisrc theme * Move MangaFire to overrides * Refactor MangaFire for multisrc * Update MangaReader changelog * Remove duplicate filter entry Co-authored-by: Druzai <70586473+Druzai@users.noreply.github.com> --------- Co-authored-by: Druzai <g9code@yandex.ru> Co-authored-by: Druzai <70586473+Druzai@users.noreply.github.com>
|
@ -0,0 +1,11 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="MangaReaderGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||||
|
<module name="tachiyomi-extensions.multisrc.main" />
|
||||||
|
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.mangareader.MangaReaderGenerator" />
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangareader" />
|
||||||
|
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangareader" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,166 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
|
||||||
|
class Entry(name: String, val id: String) : Filter.CheckBox(name) {
|
||||||
|
constructor(name: String) : this(name, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Group(
|
||||||
|
name: String,
|
||||||
|
val param: String,
|
||||||
|
values: List<Entry>,
|
||||||
|
) : Filter.Group<Entry>(name, values)
|
||||||
|
|
||||||
|
sealed class Select(
|
||||||
|
name: String,
|
||||||
|
val param: String,
|
||||||
|
private val valuesMap: Map<String, String>,
|
||||||
|
) : Filter.Select<String>(name, valuesMap.keys.toTypedArray()) {
|
||||||
|
open val selection: String
|
||||||
|
get() = valuesMap[values[state]]!!
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeFilter : Group("Type", "type[]", types)
|
||||||
|
|
||||||
|
private val types: List<Entry>
|
||||||
|
get() = listOf(
|
||||||
|
Entry("Manga", "manga"),
|
||||||
|
Entry("One-Shot", "one_shot"),
|
||||||
|
Entry("Doujinshi", "doujinshi"),
|
||||||
|
Entry("Light-Novel", "light_novel"),
|
||||||
|
Entry("Novel", "novel"),
|
||||||
|
Entry("Manhwa", "manhwa"),
|
||||||
|
Entry("Manhua", "manhua"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Genre(name: String, val id: String) : Filter.TriState(name) {
|
||||||
|
val selection: String
|
||||||
|
get() = (if (isExcluded()) "-" else "") + id
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenresFilter : Filter.Group<Genre>("Genre", genres) {
|
||||||
|
val param = "genre[]"
|
||||||
|
|
||||||
|
val combineMode: Boolean
|
||||||
|
get() = state.filter { !it.isIgnored() }.size > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private val genres: List<Genre>
|
||||||
|
get() = listOf(
|
||||||
|
Genre("Action", "1"),
|
||||||
|
Genre("Adventure", "78"),
|
||||||
|
Genre("Avant Garde", "3"),
|
||||||
|
Genre("Boys Love", "4"),
|
||||||
|
Genre("Comedy", "5"),
|
||||||
|
Genre("Demons", "77"),
|
||||||
|
Genre("Drama", "6"),
|
||||||
|
Genre("Ecchi", "7"),
|
||||||
|
Genre("Fantasy", "79"),
|
||||||
|
Genre("Girls Love", "9"),
|
||||||
|
Genre("Gourmet", "10"),
|
||||||
|
Genre("Harem", "11"),
|
||||||
|
Genre("Horror", "530"),
|
||||||
|
Genre("Isekai", "13"),
|
||||||
|
Genre("Iyashikei", "531"),
|
||||||
|
Genre("Josei", "15"),
|
||||||
|
Genre("Kids", "532"),
|
||||||
|
Genre("Magic", "539"),
|
||||||
|
Genre("Mahou Shoujo", "533"),
|
||||||
|
Genre("Martial Arts", "534"),
|
||||||
|
Genre("Mecha", "19"),
|
||||||
|
Genre("Military", "535"),
|
||||||
|
Genre("Music", "21"),
|
||||||
|
Genre("Mystery", "22"),
|
||||||
|
Genre("Parody", "23"),
|
||||||
|
Genre("Psychological", "536"),
|
||||||
|
Genre("Reverse Harem", "25"),
|
||||||
|
Genre("Romance", "26"),
|
||||||
|
Genre("School", "73"),
|
||||||
|
Genre("Sci-Fi", "28"),
|
||||||
|
Genre("Seinen", "537"),
|
||||||
|
Genre("Shoujo", "30"),
|
||||||
|
Genre("Shounen", "31"),
|
||||||
|
Genre("Slice of Life", "538"),
|
||||||
|
Genre("Space", "33"),
|
||||||
|
Genre("Sports", "34"),
|
||||||
|
Genre("Super Power", "75"),
|
||||||
|
Genre("Supernatural", "76"),
|
||||||
|
Genre("Suspense", "37"),
|
||||||
|
Genre("Thriller", "38"),
|
||||||
|
Genre("Vampire", "39"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class StatusFilter : Group("Status", "status[]", statuses)
|
||||||
|
|
||||||
|
private val statuses: List<Entry>
|
||||||
|
get() = listOf(
|
||||||
|
Entry("Completed", "completed"),
|
||||||
|
Entry("Releasing", "releasing"),
|
||||||
|
Entry("On Hiatus", "on_hiatus"),
|
||||||
|
Entry("Discontinued", "discontinued"),
|
||||||
|
Entry("Not Yet Published", "info"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class YearFilter : Group("Year", "year[]", years)
|
||||||
|
|
||||||
|
private val years: List<Entry>
|
||||||
|
get() = listOf(
|
||||||
|
Entry("2023"),
|
||||||
|
Entry("2022"),
|
||||||
|
Entry("2021"),
|
||||||
|
Entry("2020"),
|
||||||
|
Entry("2019"),
|
||||||
|
Entry("2018"),
|
||||||
|
Entry("2017"),
|
||||||
|
Entry("2016"),
|
||||||
|
Entry("2015"),
|
||||||
|
Entry("2014"),
|
||||||
|
Entry("2013"),
|
||||||
|
Entry("2012"),
|
||||||
|
Entry("2011"),
|
||||||
|
Entry("2010"),
|
||||||
|
Entry("2009"),
|
||||||
|
Entry("2008"),
|
||||||
|
Entry("2007"),
|
||||||
|
Entry("2006"),
|
||||||
|
Entry("2005"),
|
||||||
|
Entry("2004"),
|
||||||
|
Entry("2003"),
|
||||||
|
Entry("2000s"),
|
||||||
|
Entry("1990s"),
|
||||||
|
Entry("1980s"),
|
||||||
|
Entry("1970s"),
|
||||||
|
Entry("1960s"),
|
||||||
|
Entry("1950s"),
|
||||||
|
Entry("1940s"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class ChapterCountFilter : Select("Chapter Count", "minchap", chapterCounts)
|
||||||
|
|
||||||
|
private val chapterCounts
|
||||||
|
get() = mapOf(
|
||||||
|
"Any" to "",
|
||||||
|
"At least 1 chapter" to "1",
|
||||||
|
"At least 3 chapters" to "3",
|
||||||
|
"At least 5 chapters" to "5",
|
||||||
|
"At least 10 chapters" to "10",
|
||||||
|
"At least 20 chapters" to "20",
|
||||||
|
"At least 30 chapters" to "30",
|
||||||
|
"At least 50 chapters" to "50",
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter : Select("Sort", "sort", orders)
|
||||||
|
|
||||||
|
private val orders
|
||||||
|
get() = mapOf(
|
||||||
|
"Trending" to "trending",
|
||||||
|
"Recently updated" to "recently_updated",
|
||||||
|
"Recently added" to "recently_added",
|
||||||
|
"Release date" to "release_date",
|
||||||
|
"Name A-Z" to "title_az",
|
||||||
|
"Score" to "scores",
|
||||||
|
"MAL score" to "mal_scores",
|
||||||
|
"Most viewed" to "most_viewed",
|
||||||
|
"Most favourited" to "most_favourited",
|
||||||
|
)
|
|
@ -0,0 +1,79 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||||
|
|
||||||
|
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
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
object ImageInterceptor : Interceptor {
|
||||||
|
|
||||||
|
const val SCRAMBLED = "scrambled"
|
||||||
|
private const val PIECE_SIZE = 200
|
||||||
|
private const val MIN_SPLIT_COUNT = 5
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
val fragment = request.url.fragment ?: return response
|
||||||
|
if (SCRAMBLED !in fragment) return response
|
||||||
|
val offset = fragment.substringAfterLast('_').toInt()
|
||||||
|
|
||||||
|
val image = response.body.byteStream().use { descramble(it, offset) }
|
||||||
|
val body = image.toResponseBody("image/jpeg".toMediaType())
|
||||||
|
return response.newBuilder().body(body).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun descramble(image: InputStream, offset: Int): ByteArray {
|
||||||
|
// obfuscated code: https://mangafire.to/assets/t1/min/all.js
|
||||||
|
// it shuffles arrays of the image slices
|
||||||
|
|
||||||
|
val bitmap = BitmapFactory.decodeStream(image)
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
|
||||||
|
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
|
||||||
|
val pieceWidth = min(PIECE_SIZE, width.ceilDiv(MIN_SPLIT_COUNT))
|
||||||
|
val pieceHeight = min(PIECE_SIZE, height.ceilDiv(MIN_SPLIT_COUNT))
|
||||||
|
val xMax = width.ceilDiv(pieceWidth) - 1
|
||||||
|
val yMax = height.ceilDiv(pieceHeight) - 1
|
||||||
|
|
||||||
|
for (y in 0..yMax) {
|
||||||
|
for (x in 0..xMax) {
|
||||||
|
val xDst = pieceWidth * x
|
||||||
|
val yDst = pieceHeight * y
|
||||||
|
val w = min(pieceWidth, width - xDst)
|
||||||
|
val h = min(pieceHeight, height - yDst)
|
||||||
|
|
||||||
|
val xSrc = pieceWidth * when (x) {
|
||||||
|
xMax -> x // margin
|
||||||
|
else -> (xMax - x + offset) % xMax
|
||||||
|
}
|
||||||
|
val ySrc = pieceHeight * when (y) {
|
||||||
|
yMax -> y // margin
|
||||||
|
else -> (yMax - y + offset) % yMax
|
||||||
|
}
|
||||||
|
|
||||||
|
val srcRect = Rect(xSrc, ySrc, xSrc + w, ySrc + h)
|
||||||
|
val dstRect = Rect(xDst, yDst, xDst + w, yDst + h)
|
||||||
|
|
||||||
|
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
result.compress(Bitmap.CompressFormat.JPEG, 90, output)
|
||||||
|
return output.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NOTHING_TO_INLINE")
|
||||||
|
private inline fun Int.ceilDiv(other: Int) = (this + (other - 1)) / other
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
open class MangaFire(
|
||||||
|
override val lang: String,
|
||||||
|
private val langCode: String = lang,
|
||||||
|
) : MangaReader() {
|
||||||
|
override val name = "MangaFire"
|
||||||
|
|
||||||
|
override val baseUrl = "https://mangafire.to"
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
override val client = network.client.newBuilder()
|
||||||
|
.addInterceptor(ImageInterceptor)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
GET("$baseUrl/filter?sort=recently_updated&language[]=$langCode&page=$page", headers)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) =
|
||||||
|
GET("$baseUrl/filter?sort=most_viewed&language[]=$langCode&page=$page", headers)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
urlBuilder.addPathSegment("filter").apply {
|
||||||
|
addQueryParameter("keyword", query)
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
urlBuilder.addPathSegment("filter").apply {
|
||||||
|
addQueryParameter("language[]", langCode)
|
||||||
|
addQueryParameter("page", page.toString())
|
||||||
|
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is Group -> {
|
||||||
|
filter.state.forEach {
|
||||||
|
if (it.state) {
|
||||||
|
addQueryParameter(filter.param, it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Select -> {
|
||||||
|
addQueryParameter(filter.param, filter.selection)
|
||||||
|
}
|
||||||
|
is GenresFilter -> {
|
||||||
|
filter.state.forEach {
|
||||||
|
if (it.state != 0) {
|
||||||
|
addQueryParameter(filter.param, it.selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (filter.combineMode) {
|
||||||
|
addQueryParameter("genre_mode", "and")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GET(urlBuilder.build(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = ".mangas.items .inner"
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = ".page-item.active + .page-item .page-link"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) =
|
||||||
|
SManga.create().apply {
|
||||||
|
element.selectFirst("a.color-light")!!.let {
|
||||||
|
url = it.attr("href")
|
||||||
|
title = it.attr("title")
|
||||||
|
}
|
||||||
|
element.selectFirst(Evaluator.Tag("img"))!!.let {
|
||||||
|
thumbnail_url = it.attr("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
|
val root = document.selectFirst(".detail .top .wrapper")!!
|
||||||
|
val mangaTitle = root.selectFirst(Evaluator.Class("name"))!!.ownText()
|
||||||
|
title = mangaTitle
|
||||||
|
description = document.run {
|
||||||
|
val description = selectFirst(Evaluator.Class("summary"))!!.ownText()
|
||||||
|
when (val altTitle = root.selectFirst(Evaluator.Class("al-name"))!!.ownText()) {
|
||||||
|
"", mangaTitle -> description
|
||||||
|
else -> "$description\n\nAlternative Title: $altTitle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thumbnail_url = root.selectFirst(Evaluator.Tag("img"))!!.attr("src")
|
||||||
|
status = when (root.selectFirst(Evaluator.Class("status"))!!.ownText()) {
|
||||||
|
"Completed" -> SManga.COMPLETED
|
||||||
|
"Releasing" -> SManga.ONGOING
|
||||||
|
"On_hiatus" -> SManga.ON_HIATUS
|
||||||
|
"Discontinued" -> SManga.CANCELLED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
with(root.selectFirst(Evaluator.Class("more-info"))!!) {
|
||||||
|
author = selectFirst("span:contains(Author:) + span")?.text()
|
||||||
|
val type = selectFirst("span:contains(Type:) + span")?.text()
|
||||||
|
val genres = selectFirst("span:contains(Genres:) + span")?.text()
|
||||||
|
genre = listOfNotNull(type, genres).joinToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val chapterType get() = "chapter"
|
||||||
|
override val volumeType get() = "volume"
|
||||||
|
|
||||||
|
override fun chapterListRequest(mangaUrl: String, type: String): Request {
|
||||||
|
val id = mangaUrl.substringAfterLast('.')
|
||||||
|
return GET("$baseUrl/ajax/read/$id/list?viewby=$type", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||||
|
val result = json.decodeFromString<ResponseDto<ChapterListDto>>(response.body.string()).result
|
||||||
|
val container = result.parseHtml(if (isVolume) volumeType else chapterType)
|
||||||
|
?.selectFirst(".numberlist[data-lang=$langCode]")
|
||||||
|
?: return emptyList()
|
||||||
|
return container.children().map { it.child(0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val typeAndId = chapter.url.substringAfterLast('#')
|
||||||
|
return GET("$baseUrl/ajax/read/$typeAndId", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val result = json.decodeFromString<ResponseDto<PageListDto>>(response.body.string()).result
|
||||||
|
|
||||||
|
return result.pages.mapIndexed { index, image ->
|
||||||
|
val url = image.url
|
||||||
|
val offset = image.offset
|
||||||
|
val imageUrl = if (offset > 0) "$url#${ImageInterceptor.SCRAMBLED}_$offset" else url
|
||||||
|
|
||||||
|
Page(index, imageUrl = imageUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() =
|
||||||
|
FilterList(
|
||||||
|
Filter.Header("NOTE: Ignored if using text search!"),
|
||||||
|
Filter.Separator(),
|
||||||
|
TypeFilter(),
|
||||||
|
GenresFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
YearFilter(),
|
||||||
|
ChapterCountFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterListDto(private val html: String, private val link_format: String) {
|
||||||
|
fun parseHtml(type: String): Document? {
|
||||||
|
if ("LANG/$type-NUMBER" !in link_format) return null
|
||||||
|
return Jsoup.parseBodyFragment(html)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageListDto(private val images: List<List<JsonPrimitive>>) {
|
||||||
|
val pages get() = images.map { Image(it[0].content, it[2].int) }
|
||||||
|
}
|
||||||
|
|
||||||
|
class Image(val url: String, val offset: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ResponseDto<T>(val result: T)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangafire
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
|
class MangaFireFactory : SourceFactory {
|
||||||
|
override fun createSources() = listOf(
|
||||||
|
MangaFire("en"),
|
||||||
|
MangaFire("es"),
|
||||||
|
MangaFire("es-419", "es-la"),
|
||||||
|
MangaFire("fr"),
|
||||||
|
MangaFire("ja"),
|
||||||
|
MangaFire("pt"),
|
||||||
|
MangaFire("pt-BR", "pt-br"),
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,3 +1,8 @@
|
||||||
|
## 1.3.4
|
||||||
|
|
||||||
|
- Refactor and make multisrc
|
||||||
|
- Chapter page list now requires only 1 network request (those fetched in old versions still need 2)
|
||||||
|
|
||||||
## 1.3.3
|
## 1.3.3
|
||||||
|
|
||||||
- Appended `.to` to extension name
|
- Appended `.to` to extension name
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
@ -14,7 +14,7 @@ import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
object MangaReaderImageInterceptor : Interceptor {
|
object ImageInterceptor : Interceptor {
|
||||||
|
|
||||||
private val memo = hashMapOf<Int, IntArray>()
|
private val memo = hashMapOf<Int, IntArray>()
|
||||||
|
|
||||||
|
@ -22,11 +22,9 @@ object MangaReaderImageInterceptor : Interceptor {
|
||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val response = chain.proceed(request)
|
val response = chain.proceed(request)
|
||||||
|
|
||||||
val url = request.url
|
if (request.url.fragment != SCRAMBLED) return response
|
||||||
// TODO: remove the query parameter check (legacy) in later versions
|
|
||||||
if (url.fragment != SCRAMBLED && url.queryParameter("shuffled") == null) return response
|
|
||||||
|
|
||||||
val image = descramble(response.body.byteStream())
|
val image = response.body.byteStream().use(::descramble)
|
||||||
val body = image.toResponseBody("image/jpeg".toMediaType())
|
val body = image.toResponseBody("image/jpeg".toMediaType())
|
||||||
return response.newBuilder()
|
return response.newBuilder()
|
||||||
.body(body)
|
.body(body)
|
|
@ -1,15 +1,13 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.multisrc.mangareader.MangaReader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.jsonObject
|
import kotlinx.serialization.json.jsonObject
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
@ -21,62 +19,25 @@ import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.nodes.TextNode
|
import org.jsoup.nodes.TextNode
|
||||||
import org.jsoup.select.Evaluator
|
import org.jsoup.select.Evaluator
|
||||||
import uy.kohesive.injekt.Injekt
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
open class MangaReader(
|
open class MangaReader(
|
||||||
override val lang: String,
|
override val lang: String,
|
||||||
) : ConfigurableSource, ParsedHttpSource() {
|
) : MangaReader() {
|
||||||
override val name = "MangaReader"
|
override val name = "MangaReader"
|
||||||
|
|
||||||
override val baseUrl = "https://mangareader.to"
|
override val baseUrl = "https://mangareader.to"
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.client.newBuilder()
|
override val client = network.client.newBuilder()
|
||||||
.addInterceptor(MangaReaderImageInterceptor)
|
.addInterceptor(ImageInterceptor)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun MangasPage.insertVolumeEntries(): MangasPage {
|
|
||||||
if (preferences.showVolume.not()) return this
|
|
||||||
val list = mangas.ifEmpty { return this }
|
|
||||||
val newList = ArrayList<SManga>(list.size * 2)
|
|
||||||
for (manga in list) {
|
|
||||||
val volume = SManga.create().apply {
|
|
||||||
url = manga.url + VOLUME_URL_SUFFIX
|
|
||||||
title = VOLUME_TITLE_PREFIX + manga.title
|
|
||||||
thumbnail_url = manga.thumbnail_url
|
|
||||||
}
|
|
||||||
newList.add(manga)
|
|
||||||
newList.add(volume)
|
|
||||||
}
|
|
||||||
return MangasPage(newList, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response) = super.latestUpdatesParse(response).insertVolumeEntries()
|
|
||||||
override fun popularMangaParse(response: Response) = super.popularMangaParse(response).insertVolumeEntries()
|
|
||||||
override fun searchMangaParse(response: Response) = super.searchMangaParse(response).insertVolumeEntries()
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) =
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
|
GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
|
||||||
|
|
||||||
override fun latestUpdatesSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element) =
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) =
|
override fun popularMangaRequest(page: Int) =
|
||||||
GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
|
GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
|
||||||
|
|
||||||
override fun popularMangaSelector() = searchMangaSelector()
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element) =
|
|
||||||
searchMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
val urlBuilder = baseUrl.toHttpUrl().newBuilder()
|
||||||
if (query.isNotBlank()) {
|
if (query.isNotBlank()) {
|
||||||
|
@ -106,7 +67,7 @@ open class MangaReader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Request.Builder().url(urlBuilder.build()).headers(headers).build()
|
return GET(urlBuilder.build(), headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
|
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
|
||||||
|
@ -145,10 +106,9 @@ open class MangaReader(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||||
url = document.location().removePrefix(baseUrl)
|
|
||||||
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
|
val root = document.selectFirst(Evaluator.Id("ani_detail"))!!
|
||||||
val mangaTitle = root.selectFirst(Evaluator.Tag("h2"))!!.ownText()
|
val mangaTitle = root.selectFirst(Evaluator.Tag("h2"))!!.ownText()
|
||||||
title = if (url.endsWith(VOLUME_URL_SUFFIX)) VOLUME_TITLE_PREFIX + mangaTitle else mangaTitle
|
title = mangaTitle
|
||||||
description = root.run {
|
description = root.run {
|
||||||
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
val description = selectFirst(Evaluator.Class("description"))!!.ownText()
|
||||||
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
|
when (val altTitle = selectFirst(Evaluator.Class("manga-name-or"))!!.ownText()) {
|
||||||
|
@ -171,70 +131,45 @@ open class MangaReader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListRequest(manga: SManga): Request {
|
override val chapterType get() = "chap"
|
||||||
val url = manga.url
|
override val volumeType get() = "vol"
|
||||||
val id = url.removeSuffix(VOLUME_URL_SUFFIX).substringAfterLast('-')
|
|
||||||
val type = if (url.endsWith(VOLUME_URL_SUFFIX)) "vol" else "chap"
|
override fun chapterListRequest(mangaUrl: String, type: String): Request {
|
||||||
|
val id = mangaUrl.substringAfterLast('-')
|
||||||
return GET("$baseUrl/ajax/manga/reading-list/$id?readingBy=$type", headers)
|
return GET("$baseUrl/ajax/manga/reading-list/$id?readingBy=$type", headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListSelector() = "#$lang-chapters .item"
|
override fun parseChapterElements(response: Response, isVolume: Boolean): List<Element> {
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val isVolume = response.request.url.queryParameter("readingBy") == "vol"
|
|
||||||
val container = response.parseHtmlProperty().run {
|
val container = response.parseHtmlProperty().run {
|
||||||
val type = if (isVolume) "volumes" else "chapters"
|
val type = if (isVolume) "volumes" else "chapters"
|
||||||
selectFirst(Evaluator.Id("$lang-$type")) ?: return emptyList()
|
selectFirst(Evaluator.Id("$lang-$type")) ?: return emptyList()
|
||||||
}
|
}
|
||||||
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
return container.children()
|
||||||
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
|
||||||
return container.children().map { chapterFromElement(it, abbrPrefix, fullPrefix) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) =
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.fromCallable {
|
||||||
throw UnsupportedOperationException("Not used.")
|
val typeAndId = chapter.url.substringAfterLast('#', "").ifEmpty {
|
||||||
|
val document = client.newCall(pageListRequest(chapter)).execute().asJsoup()
|
||||||
private fun chapterFromElement(element: Element, abbrPrefix: String, fullPrefix: String) =
|
val wrapper = document.selectFirst(Evaluator.Id("wrapper"))!!
|
||||||
SChapter.create().apply {
|
wrapper.attr("data-reading-by") + '/' + wrapper.attr("data-reading-id")
|
||||||
val number = element.attr("data-number")
|
|
||||||
chapter_number = number.toFloatOrNull() ?: -1f
|
|
||||||
element.selectFirst(Evaluator.Tag("a"))!!.let {
|
|
||||||
url = it.attr("href")
|
|
||||||
name = run {
|
|
||||||
val name = it.attr("title")
|
|
||||||
val prefix = "$abbrPrefix $number: "
|
|
||||||
if (name.startsWith(prefix).not()) return@run name
|
|
||||||
val realName = name.removePrefix(prefix)
|
|
||||||
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
val ajaxUrl = "$baseUrl/ajax/image/list/$typeAndId?quality=${preferences.quality}"
|
||||||
|
client.newCall(GET(ajaxUrl, headers)).execute().let(::pageListParse)
|
||||||
|
}
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
val ajaxUrl = document.selectFirst(Evaluator.Id("wrapper"))!!.run {
|
val pageDocument = response.parseHtmlProperty()
|
||||||
val readingBy = attr("data-reading-by")
|
|
||||||
val readingId = attr("data-reading-id")
|
|
||||||
"$baseUrl/ajax/image/list/$readingBy/$readingId?quality=${preferences.quality}"
|
|
||||||
}
|
|
||||||
|
|
||||||
val pageDocument = client.newCall(GET(ajaxUrl, headers)).execute().parseHtmlProperty()
|
|
||||||
|
|
||||||
return pageDocument.getElementsByClass("iv-card").mapIndexed { index, img ->
|
return pageDocument.getElementsByClass("iv-card").mapIndexed { index, img ->
|
||||||
val url = img.attr("data-url")
|
val url = img.attr("data-url")
|
||||||
val imageUrl = if (img.hasClass("shuffled")) "$url#${MangaReaderImageInterceptor.SCRAMBLED}" else url
|
val imageUrl = if (img.hasClass("shuffled")) "$url#${ImageInterceptor.SCRAMBLED}" else url
|
||||||
Page(index, imageUrl = imageUrl)
|
Page(index, imageUrl = imageUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) =
|
|
||||||
throw UnsupportedOperationException("Not used")
|
|
||||||
|
|
||||||
private val preferences by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
getPreferences(screen.context).forEach(screen::addPreference)
|
getPreferences(screen.context).forEach(screen::addPreference)
|
||||||
|
super.setupPreferenceScreen(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilterList() =
|
override fun getFilterList() =
|
|
@ -3,39 +3,24 @@ package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
|
|
||||||
fun getPreferences(context: Context) = arrayOf(
|
fun getPreferences(context: Context) = arrayOf(
|
||||||
|
|
||||||
ListPreference(context).apply {
|
ListPreference(context).apply {
|
||||||
key = QUALITY_PREF
|
key = QUALITY_PREF
|
||||||
title = "Image quality"
|
title = "Image quality"
|
||||||
summary = "Selected: %s\n" +
|
summary = "%s\n" +
|
||||||
"Changes will not be applied to chapters that are already loaded or read " +
|
"Changes will not be applied to chapters that are already loaded or read " +
|
||||||
"until you clear the chapter cache."
|
"until you clear the chapter cache."
|
||||||
entries = arrayOf("Low", "Medium", "High")
|
entries = arrayOf("Low", "Medium", "High")
|
||||||
entryValues = arrayOf("low", QUALITY_MEDIUM, "high")
|
entryValues = arrayOf("low", QUALITY_MEDIUM, "high")
|
||||||
setDefaultValue(QUALITY_MEDIUM)
|
setDefaultValue(QUALITY_MEDIUM)
|
||||||
},
|
},
|
||||||
|
|
||||||
SwitchPreferenceCompat(context).apply {
|
|
||||||
key = SHOW_VOLUME_PREF
|
|
||||||
title = "Show manga in volumes in search result"
|
|
||||||
setDefaultValue(false)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val SharedPreferences.quality
|
val SharedPreferences.quality
|
||||||
get() =
|
get() =
|
||||||
getString(QUALITY_PREF, QUALITY_MEDIUM)!!
|
getString(QUALITY_PREF, QUALITY_MEDIUM)!!
|
||||||
|
|
||||||
val SharedPreferences.showVolume
|
|
||||||
get() =
|
|
||||||
getBoolean(SHOW_VOLUME_PREF, false)
|
|
||||||
|
|
||||||
private const val QUALITY_PREF = "quality"
|
private const val QUALITY_PREF = "quality"
|
||||||
private const val QUALITY_MEDIUM = "medium"
|
private const val QUALITY_MEDIUM = "medium"
|
||||||
private const val SHOW_VOLUME_PREF = "show_volume"
|
|
||||||
|
|
||||||
const val VOLUME_URL_SUFFIX = "#vol"
|
|
||||||
const val VOLUME_TITLE_PREFIX = "[VOL] "
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.mangareader
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
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 okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import org.jsoup.select.Evaluator
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
abstract class MangaReader : HttpSource(), ConfigurableSource {
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
final override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||||
|
|
||||||
|
final override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
var entries = document.select(searchMangaSelector()).map(::searchMangaFromElement)
|
||||||
|
if (preferences.getBoolean(SHOW_VOLUME_PREF, false)) {
|
||||||
|
entries = entries.flatMapTo(ArrayList(entries.size * 2)) { manga ->
|
||||||
|
val volume = SManga.create().apply {
|
||||||
|
url = manga.url + VOLUME_URL_SUFFIX
|
||||||
|
title = VOLUME_TITLE_PREFIX + manga.title
|
||||||
|
thumbnail_url = manga.thumbnail_url
|
||||||
|
}
|
||||||
|
listOf(manga, volume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val hasNextPage = document.selectFirst(searchMangaNextPageSelector()) != null
|
||||||
|
return MangasPage(entries, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.removeSuffix(VOLUME_URL_SUFFIX)
|
||||||
|
|
||||||
|
abstract fun searchMangaSelector(): String
|
||||||
|
|
||||||
|
abstract fun searchMangaNextPageSelector(): String
|
||||||
|
|
||||||
|
abstract fun searchMangaFromElement(element: Element): SManga
|
||||||
|
|
||||||
|
abstract fun mangaDetailsParse(document: Document): SManga
|
||||||
|
|
||||||
|
final override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val manga = mangaDetailsParse(document)
|
||||||
|
if (response.request.url.fragment == VOLUME_URL_FRAGMENT) {
|
||||||
|
manga.title = VOLUME_TITLE_PREFIX + manga.title
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract val chapterType: String
|
||||||
|
abstract val volumeType: String
|
||||||
|
|
||||||
|
abstract fun chapterListRequest(mangaUrl: String, type: String): Request
|
||||||
|
|
||||||
|
abstract fun parseChapterElements(response: Response, isVolume: Boolean): List<Element>
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
final override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.fromCallable {
|
||||||
|
val path = manga.url
|
||||||
|
val isVolume = path.endsWith(VOLUME_URL_SUFFIX)
|
||||||
|
val type = if (isVolume) volumeType else chapterType
|
||||||
|
val request = chapterListRequest(path.removeSuffix(VOLUME_URL_SUFFIX), type)
|
||||||
|
val response = client.newCall(request).execute()
|
||||||
|
|
||||||
|
val abbrPrefix = if (isVolume) "Vol" else "Chap"
|
||||||
|
val fullPrefix = if (isVolume) "Volume" else "Chapter"
|
||||||
|
val linkSelector = Evaluator.Tag("a")
|
||||||
|
parseChapterElements(response, isVolume).map { element ->
|
||||||
|
SChapter.create().apply {
|
||||||
|
val number = element.attr("data-number")
|
||||||
|
chapter_number = number.toFloatOrNull() ?: -1f
|
||||||
|
|
||||||
|
val link = element.selectFirst(linkSelector)!!
|
||||||
|
name = run {
|
||||||
|
val name = link.text()
|
||||||
|
val prefix = "$abbrPrefix $number: "
|
||||||
|
if (!name.startsWith(prefix)) return@run name
|
||||||
|
val realName = name.removePrefix(prefix)
|
||||||
|
if (realName.contains(number)) realName else "$fullPrefix $number: $realName"
|
||||||
|
}
|
||||||
|
setUrlWithoutDomain(link.attr("href") + '#' + type + '/' + element.attr("data-id"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun getChapterUrl(chapter: SChapter) = baseUrl + chapter.url.substringBeforeLast('#')
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
val preferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = SHOW_VOLUME_PREF
|
||||||
|
title = "Show volume entries in search result"
|
||||||
|
setDefaultValue(false)
|
||||||
|
}.let(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SHOW_VOLUME_PREF = "show_volume"
|
||||||
|
|
||||||
|
private const val VOLUME_URL_FRAGMENT = "vol"
|
||||||
|
private const val VOLUME_URL_SUFFIX = "#" + VOLUME_URL_FRAGMENT
|
||||||
|
private const val VOLUME_TITLE_PREFIX = "[VOL] "
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package eu.kanade.tachiyomi.multisrc.mangareader
|
||||||
|
|
||||||
|
import generator.ThemeSourceData.MultiLang
|
||||||
|
import generator.ThemeSourceGenerator
|
||||||
|
|
||||||
|
class MangaReaderGenerator : ThemeSourceGenerator {
|
||||||
|
override val themeClass = "MangaReader"
|
||||||
|
override val themePkg = "mangareader"
|
||||||
|
override val baseVersionCode = 1
|
||||||
|
override val sources = listOf(
|
||||||
|
MultiLang(
|
||||||
|
name = "MangaReader",
|
||||||
|
baseUrl = "https://mangareader.to",
|
||||||
|
langs = listOf("en", "fr", "ja", "ko", "zh"),
|
||||||
|
isNsfw = true,
|
||||||
|
pkgName = "mangareaderto",
|
||||||
|
overrideVersionCode = 3,
|
||||||
|
),
|
||||||
|
MultiLang(
|
||||||
|
name = "MangaFire",
|
||||||
|
baseUrl = "https://mangafire.to",
|
||||||
|
langs = listOf("en", "es", "es-419", "fr", "ja", "pt", "pt-BR"),
|
||||||
|
isNsfw = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
MangaReaderGenerator().createAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
|
@ -1,13 +0,0 @@
|
||||||
apply plugin: 'com.android.application'
|
|
||||||
apply plugin: 'kotlin-android'
|
|
||||||
apply plugin: 'kotlinx-serialization'
|
|
||||||
|
|
||||||
ext {
|
|
||||||
extName = 'MangaReader.to'
|
|
||||||
pkgNameSuffix = 'all.mangareaderto'
|
|
||||||
extClass = '.MangaReaderFactory'
|
|
||||||
extVersionCode = 3
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|