fuck sizurpp (#4439)
* fuck sizurpp * update url Co-authored-by: Rani Sargees <rani.sargees@gmail.com>
This commit is contained in:
parent
15c6ade5fb
commit
910716b913
|
@ -0,0 +1,16 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Japscan'
|
||||||
|
pkgNameSuffix = 'fr.japscan'
|
||||||
|
extClass = '.Japscan'
|
||||||
|
extVersionCode = 21
|
||||||
|
libVersion = '1.2'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'org.apache.commons:commons-lang3:3.8.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
|
@ -0,0 +1,421 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.fr.japscan
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import android.support.v7.preference.ListPreference
|
||||||
|
import android.support.v7.preference.PreferenceScreen
|
||||||
|
import android.view.View
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
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.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import okhttp3.FormBody
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import org.jsoup.nodes.Element
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class Japscan : ConfigurableSource, ParsedHttpSource() {
|
||||||
|
|
||||||
|
override val id: Long = 11
|
||||||
|
|
||||||
|
override val name = "Japscan"
|
||||||
|
|
||||||
|
override val baseUrl = "https://www.japscan.se"
|
||||||
|
|
||||||
|
override val lang = "fr"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
override val client: OkHttpClient = network.cloudflareClient.newBuilder().addInterceptor { chain ->
|
||||||
|
val indicator = "&wvsc"
|
||||||
|
val cleanupjs = "var db=document.body,chl=db.children;for(db.appendChild(document.getElementsByTagName('CNV-VV')[0]);'CNV-VV'!=chl[0].tagName;)db.removeChild(chl[0]);for(var i of[].slice.call(chl[0].all_canvas)){i.style.maxWidth=(i.width+\"px\")}window.variable={w:chl[0].all_canvas[0].width,h:chl[0].all_canvas[0].height};"
|
||||||
|
val request = chain.request()
|
||||||
|
val url = request.url().toString()
|
||||||
|
|
||||||
|
val newRequest = request.newBuilder()
|
||||||
|
.url(url.substringBefore(indicator))
|
||||||
|
.build()
|
||||||
|
val response = chain.proceed(newRequest)
|
||||||
|
if (!url.endsWith(indicator)) return@addInterceptor response
|
||||||
|
// Webview screenshotting code
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
var webView: WebView? = null
|
||||||
|
var height = 0
|
||||||
|
var width = 0
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
val webview = WebView(Injekt.get<Application>())
|
||||||
|
webView = webview
|
||||||
|
webview.settings.javaScriptEnabled = true
|
||||||
|
webview.settings.domStorageEnabled = true
|
||||||
|
webview.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||||
|
|
||||||
|
webview.webChromeClient = object : WebChromeClient() {
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onProgressChanged(view: WebView, progress: Int) {
|
||||||
|
if (progress == 100) {
|
||||||
|
view.evaluateJavascript(cleanupjs) {
|
||||||
|
if (it.contains('{')) {
|
||||||
|
val j = JsonParser().parse(it).asJsonObject
|
||||||
|
width = j["w"].asInt
|
||||||
|
height = j["h"].asInt
|
||||||
|
latch.countDown()
|
||||||
|
} else {
|
||||||
|
webview.loadUrl(url.replace("&wvsc", ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
webview.loadUrl(url.replace("&wvsc", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
latch.await()
|
||||||
|
|
||||||
|
// webView!!.isDrawingCacheEnabled = true
|
||||||
|
|
||||||
|
webView!!.measure(width + 100, height + 100)
|
||||||
|
webView!!.layout(0, 0, width + 100, height + 100)
|
||||||
|
Thread.sleep(350)
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||||
|
var canvas = Canvas(bitmap)
|
||||||
|
webView!!.draw(canvas)
|
||||||
|
|
||||||
|
// val bitmap: Bitmap = webView!!.drawingCache
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, output)
|
||||||
|
|
||||||
|
val rb = ResponseBody.create(MediaType.parse("image/png"), output.toByteArray())
|
||||||
|
response.newBuilder().body(rb).build()
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val dateFormat by lazy {
|
||||||
|
SimpleDateFormat("dd MMM yyyy", Locale.US)
|
||||||
|
}
|
||||||
|
private const val SHOW_SPOILER_CHAPTERS_Title = "Les chapitres en Anglais ou non traduit sont upload en tant que \" Spoilers \" sur Japscan"
|
||||||
|
private const val SHOW_SPOILER_CHAPTERS = "JAPSCAN_SPOILER_CHAPTERS"
|
||||||
|
private val prefsEntries = arrayOf("Montrer uniquement les chapitres traduit en Français", "Montrer les chapitres spoiler")
|
||||||
|
private val prefsEntryValues = arrayOf("hide", "show")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun chapterListPref() = preferences.getString(SHOW_SPOILER_CHAPTERS, "hide")
|
||||||
|
|
||||||
|
// Popular
|
||||||
|
override fun popularMangaRequest(page: Int): Request {
|
||||||
|
return GET("$baseUrl/mangas/", headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
pageNumberDoc = document
|
||||||
|
|
||||||
|
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||||
|
popularMangaFromElement(element)
|
||||||
|
}
|
||||||
|
val hasNextPage = false
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector(): String? = null
|
||||||
|
override fun popularMangaSelector() = "#top_mangas_week li > span"
|
||||||
|
override fun popularMangaFromElement(element: Element): SManga {
|
||||||
|
val manga = SManga.create()
|
||||||
|
element.select("a").first().let {
|
||||||
|
manga.setUrlWithoutDomain(it.attr("href"))
|
||||||
|
manga.title = it.text()
|
||||||
|
|
||||||
|
val s = StringUtils.stripAccents(it.text())
|
||||||
|
.replace("[\\W]".toRegex(), "-")
|
||||||
|
.replace("[-]{2,}".toRegex(), "-")
|
||||||
|
.replace("^-|-$".toRegex(), "")
|
||||||
|
manga.thumbnail_url = "$baseUrl/imgs/mangas/$s.jpg".toLowerCase(Locale.ROOT)
|
||||||
|
}
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
// Latest
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request {
|
||||||
|
return GET(baseUrl, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val mangas = document.select(latestUpdatesSelector())
|
||||||
|
.distinctBy { element -> element.select("a").attr("href") }
|
||||||
|
.map { element ->
|
||||||
|
latestUpdatesFromElement(element)
|
||||||
|
}
|
||||||
|
val hasNextPage = false
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector(): String? = null
|
||||||
|
override fun latestUpdatesSelector() = "#chapters > div > h3.text-truncate"
|
||||||
|
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||||
|
|
||||||
|
// Search
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query.isEmpty()) {
|
||||||
|
val uri = Uri.parse(baseUrl).buildUpon()
|
||||||
|
.appendPath("mangas")
|
||||||
|
filters.forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is TextField -> uri.appendPath(((page - 1) + filter.state.toInt()).toString())
|
||||||
|
is PageList -> uri.appendPath(((page - 1) + filter.values[filter.state]).toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return GET(uri.toString(), headers)
|
||||||
|
} else {
|
||||||
|
val formBody = FormBody.Builder()
|
||||||
|
.add("search", query)
|
||||||
|
.build()
|
||||||
|
val searchHeaders = headers.newBuilder()
|
||||||
|
.add("X-Requested-With", "XMLHttpRequest")
|
||||||
|
.build()
|
||||||
|
return POST("$baseUrl/live-search/", searchHeaders, formBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector(): String? = "li.page-item:last-child:not(li.active)"
|
||||||
|
override fun searchMangaSelector(): String = "div.card div.p-2, a.result-link"
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
|
if ("live-search" in response.request().url().toString()) {
|
||||||
|
val body = response.body()!!.string()
|
||||||
|
val json = JsonParser().parse(body).asJsonArray
|
||||||
|
val mangas = json.map { jsonElement ->
|
||||||
|
searchMangaFromJson(jsonElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = false
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
} else {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
|
||||||
|
val mangas = document.select(searchMangaSelector()).map { element ->
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hasNextPage = searchMangaNextPageSelector()?.let { selector ->
|
||||||
|
document.select(selector).first()
|
||||||
|
} != null
|
||||||
|
|
||||||
|
return MangasPage(mangas, hasNextPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
|
thumbnail_url = element.select("img").attr("abs:src")
|
||||||
|
element.select("p a").let {
|
||||||
|
title = it.text()
|
||||||
|
url = it.attr("href")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMangaFromJson(jsonElement: JsonElement): SManga = SManga.create().apply {
|
||||||
|
title = jsonElement["name"].string
|
||||||
|
url = jsonElement["url"].string
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document): SManga {
|
||||||
|
val infoElement = document.select("div#main > .card > .card-body").first()
|
||||||
|
|
||||||
|
val manga = SManga.create()
|
||||||
|
manga.thumbnail_url = "$baseUrl/${infoElement.select(".d-flex > div.m-2:eq(0) > img").attr("src")}"
|
||||||
|
|
||||||
|
infoElement.select(".d-flex > div.m-2:eq(1) > p.mb-2").forEachIndexed { _, el ->
|
||||||
|
when (el.select("span").text().trim()) {
|
||||||
|
"Auteur(s):" -> manga.author = el.text().replace("Auteur(s):", "").trim()
|
||||||
|
"Artiste(s):" -> manga.artist = el.text().replace("Artiste(s):", "").trim()
|
||||||
|
"Genre(s):" -> manga.genre = el.text().replace("Genre(s):", "").trim()
|
||||||
|
"Statut:" -> manga.status = el.text().replace("Statut:", "").trim().let {
|
||||||
|
parseStatus(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manga.description = infoElement.select("> p").text().orEmpty()
|
||||||
|
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseStatus(status: String) = when {
|
||||||
|
status.contains("En Cours") -> SManga.ONGOING
|
||||||
|
status.contains("Terminé") -> SManga.COMPLETED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "#chapters_list > div.collapse > div.chapters_list" +
|
||||||
|
if (chapterListPref() == "hide") { ":not(:has(.badge:contains(SPOILER),.badge:contains(RAW),.badge:contains(VUS)))" } else { "" }
|
||||||
|
// JapScan sometimes uploads some "spoiler preview" chapters, containing 2 or 3 untranslated pictures taken from a raw. Sometimes they also upload full RAWs/US versions and replace them with a translation as soon as available.
|
||||||
|
// Those have a span.badge "SPOILER" or "RAW". The additional pseudo selector makes sure to exclude these from the chapter list.
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element): SChapter {
|
||||||
|
val urlElement = element.select("a").first()
|
||||||
|
|
||||||
|
val chapter = SChapter.create()
|
||||||
|
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||||
|
chapter.name = urlElement.ownText()
|
||||||
|
// Using ownText() doesn't include childs' text, like "VUS" or "RAW" badges, in the chapter name.
|
||||||
|
chapter.date_upload = element.select("> span").text().trim().let { parseChapterDate(it) }
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseChapterDate(date: String): Long {
|
||||||
|
return try {
|
||||||
|
dateFormat.parse(date)?.time ?: 0
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> {
|
||||||
|
return if (document.getElementsByTag("script").size> 12) { // scrambled images, webview screenshotting
|
||||||
|
document.getElementsByTag("option").mapIndexed { i, it -> Page(i, "", baseUrl + it.attr("value") + "&wvsc") }
|
||||||
|
} else {
|
||||||
|
// unscrambled images, check for single page
|
||||||
|
val zjsurl = document.getElementsByTag("script").first { it.attr("src").contains("zjs", ignoreCase = true) }.attr("src")
|
||||||
|
val zjs = client.newCall(GET(baseUrl + zjsurl, headers)).execute().body()!!.string()
|
||||||
|
if ((zjs.toLowerCase().split("new image").size - 1) == 1) { // single page, webview request dumping
|
||||||
|
val pagecount = document.getElementsByTag("option").size
|
||||||
|
val pages = ArrayList<Page>()
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
val dummyimage = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
|
||||||
|
val dummystream = ByteArrayOutputStream()
|
||||||
|
dummyimage.compress(Bitmap.CompressFormat.JPEG, 100, dummystream)
|
||||||
|
|
||||||
|
handler.post {
|
||||||
|
val webview = WebView(Injekt.get<Application>())
|
||||||
|
webview.settings.javaScriptEnabled = true
|
||||||
|
webview.settings.domStorageEnabled = true
|
||||||
|
webview.webViewClient = object : WebViewClient() {
|
||||||
|
override fun shouldInterceptRequest(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): WebResourceResponse? {
|
||||||
|
if (request.url.toString().startsWith("https://c.")) {
|
||||||
|
pages.add(Page(pages.size, "", request.url.toString()))
|
||||||
|
if (pages.size == pagecount) { latch.countDown() }
|
||||||
|
return WebResourceResponse("image/jpeg", "UTF-8", ByteArrayInputStream(dummystream.toByteArray()))
|
||||||
|
}
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
webview.loadUrl(baseUrl + document.getElementsByTag("option").first().attr("value"))
|
||||||
|
}
|
||||||
|
latch.await()
|
||||||
|
return pages
|
||||||
|
} else { // page by page, just do webview screenshotting because it's easier
|
||||||
|
document.getElementsByTag("option").mapIndexed { i, it -> Page(i, "", baseUrl + it.attr("value") + "&wvsc") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document): String = ""
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
private class TextField(name: String) : Filter.Text(name)
|
||||||
|
|
||||||
|
private class PageList(pages: Array<Int>) : Filter.Select<Int>("Page #", arrayOf(0, *pages))
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList {
|
||||||
|
val totalPages = pageNumberDoc?.select("li.page-item:last-child a")?.text()
|
||||||
|
val pagelist = mutableListOf<Int>()
|
||||||
|
return if (!totalPages.isNullOrEmpty()) {
|
||||||
|
for (i in 0 until totalPages.toInt()) {
|
||||||
|
pagelist.add(i + 1)
|
||||||
|
}
|
||||||
|
FilterList(
|
||||||
|
Filter.Header("Page alphabétique"),
|
||||||
|
PageList(pagelist.toTypedArray())
|
||||||
|
)
|
||||||
|
} else FilterList(
|
||||||
|
Filter.Header("Page alphabétique"),
|
||||||
|
TextField("Page #"),
|
||||||
|
Filter.Header("Appuyez sur reset pour la liste")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pageNumberDoc: Document? = null
|
||||||
|
|
||||||
|
// Prefs
|
||||||
|
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||||
|
val chapterListPref = androidx.preference.ListPreference(screen.context).apply {
|
||||||
|
key = SHOW_SPOILER_CHAPTERS_Title
|
||||||
|
title = SHOW_SPOILER_CHAPTERS_Title
|
||||||
|
entries = prefsEntries
|
||||||
|
entryValues = prefsEntryValues
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = this.findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(SHOW_SPOILER_CHAPTERS, entry).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.addPreference(chapterListPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val chapterListPref = ListPreference(screen.context).apply {
|
||||||
|
key = SHOW_SPOILER_CHAPTERS_Title
|
||||||
|
title = SHOW_SPOILER_CHAPTERS_Title
|
||||||
|
entries = prefsEntries
|
||||||
|
entryValues = prefsEntryValues
|
||||||
|
summary = "%s"
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
val selected = newValue as String
|
||||||
|
val index = this.findIndexOfValue(selected)
|
||||||
|
val entry = entryValues[index] as String
|
||||||
|
preferences.edit().putString(SHOW_SPOILER_CHAPTERS, entry).commit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
screen.addPreference(chapterListPref)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue