New extension GMANGA (#5274)
* Initial commit of GMANGA * Enable text search * Implement search filters * Fix bug unable to render pages of non-webp chapters * Replace default user-agent with a more generic one * Add preference to select picking most viewed scan or just show all * Implement rate limiting to avoid IP ban * Translate messages to Arabic * Split functionality to different files
This commit is contained in:
parent
de178465c7
commit
a552a5267b
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,17 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'GMANGA'
|
||||
pkgNameSuffix = 'ar.gmanga'
|
||||
extClass = '.Gmanga'
|
||||
extVersionCode = 1
|
||||
libVersion = '1.2'
|
||||
containsNsfw = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':lib-ratelimit')
|
||||
}
|
||||
|
||||
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.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
|
@ -0,0 +1,181 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING
|
||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING_SHOW_POPULAR
|
||||
import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
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 eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
|
||||
class Gmanga : ConfigurableSource, HttpSource() {
|
||||
|
||||
private val domain: String = "gmanga.me"
|
||||
|
||||
override val baseUrl: String = "https://$domain"
|
||||
|
||||
override val lang: String = "ar"
|
||||
|
||||
override val name: String = "GMANGA"
|
||||
|
||||
override val supportsLatest: Boolean = true
|
||||
|
||||
private val gson = Gson()
|
||||
|
||||
private val preferences = GmangaPreferences(id)
|
||||
|
||||
private val rateLimitInterceptor = RateLimitInterceptor(4)
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
.addNetworkInterceptor(rateLimitInterceptor)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", USER_AGENT)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = preferences.setupPreferenceScreen(screen)
|
||||
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) = preferences.setupPreferenceScreen(screen)
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val mangaId = manga.url.substringAfterLast("/")
|
||||
return GET("$baseUrl/api/mangas/$mangaId/releases", headers)
|
||||
}
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val data = decryptResponse(response)
|
||||
|
||||
val chapters: List<JsonArray> = buildList {
|
||||
val allChapters = data["rows"][0]["rows"].asJsonArray.map { it.asJsonArray }
|
||||
|
||||
when (preferences.getString(PREF_CHAPTER_LISTING)) {
|
||||
PREF_CHAPTER_LISTING_SHOW_POPULAR -> addAll(
|
||||
allChapters.groupBy { it.asJsonArray[6].asFloat }
|
||||
.map { (_: Float, versions: List<JsonArray>) -> versions.maxByOrNull { it[4].asLong }!! }
|
||||
)
|
||||
else -> addAll(allChapters)
|
||||
}
|
||||
}
|
||||
|
||||
return chapters.map {
|
||||
SChapter.create().apply {
|
||||
chapter_number = it[6].asFloat
|
||||
|
||||
val chapterName = it[8].asString.let { if (it.trim() != "") " - $it" else "" }
|
||||
|
||||
url = "/r/${it[0]}"
|
||||
name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
|
||||
date_upload = it[3].asLong * 1000
|
||||
scanlator = it[10].asString
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val data = gson.fromJson<JsonObject>(response.asJsoup().select(".js-react-on-rails-component").html())
|
||||
return MangasPage(
|
||||
data["mangaDataAction"]["newMangas"].asJsonArray.map {
|
||||
SManga.create().apply {
|
||||
url = "/mangas/${it["id"].asString}"
|
||||
title = it["title"].asString
|
||||
val thumbnail = "medium_${it["cover"].asString.substringBeforeLast(".")}.webp"
|
||||
thumbnail_url = "https://media.$domain/uploads/manga/cover/${it["id"].asString}/$thumbnail"
|
||||
}
|
||||
},
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/mangas/latest", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val data = gson.fromJson<JsonObject>(response.asJsoup().select(".js-react-on-rails-component").html())
|
||||
val mangaData = data["mangaDataAction"]["mangaData"].asJsonObject
|
||||
return SManga.create().apply {
|
||||
description = mangaData["summary"].nullString ?: ""
|
||||
artist = mangaData["artists"].asJsonArray.joinToString(", ") { it.asJsonObject["name"].asString }
|
||||
author = mangaData["authors"].asJsonArray.joinToString(", ") { it.asJsonObject["name"].asString }
|
||||
genre = mangaData["categories"].asJsonArray.joinToString(", ") { it.asJsonObject["name"].asString }
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val url = response.request().url().toString()
|
||||
val data = gson.fromJson<JsonObject>(response.asJsoup().select(".js-react-on-rails-component").html())
|
||||
val releaseData = data["readerDataAction"]["readerData"]["release"].asJsonObject
|
||||
|
||||
val hasWebP = releaseData["webp_pages"].asJsonArray.size() > 0
|
||||
return releaseData[if (hasWebP) "webp_pages" else "pages"].asJsonArray.map { it.asString }.mapIndexed { index, pageUri ->
|
||||
Page(
|
||||
index,
|
||||
"$url#page_$index",
|
||||
"https://media.$domain/uploads/releases/${releaseData["storage_key"].asString}/mq${if (hasWebP) "_webp" else ""}/$pageUri"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val data = decryptResponse(response)
|
||||
val mangas = data["mangas"].asJsonArray
|
||||
return MangasPage(
|
||||
mangas.asJsonArray.map {
|
||||
SManga.create().apply {
|
||||
url = "/mangas/${it["id"].asString}"
|
||||
title = it["title"].asString
|
||||
val thumbnail = "medium_${it["cover"].asString.substringBeforeLast(".")}.webp"
|
||||
thumbnail_url = "https://media.$domain/uploads/manga/cover/${it["id"].asString}/$thumbnail"
|
||||
}
|
||||
},
|
||||
mangas.size() == 50
|
||||
)
|
||||
}
|
||||
|
||||
private fun decryptResponse(response: Response): JsonObject {
|
||||
val encryptedData = gson.fromJson<JsonObject>(response.body()!!.string())["data"].asString
|
||||
val decryptedData = decrypt(encryptedData)
|
||||
return gson.fromJson(decryptedData)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GmangaFilters.buildSearchPayload(page, query, filters).let {
|
||||
val body = RequestBody.create(MEDIA_TYPE, it.toString())
|
||||
POST("$baseUrl/api/mangas/search", headers, body)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = GmangaFilters.getFilterList()
|
||||
|
||||
companion object {
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36"
|
||||
private val MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Build
|
||||
import java.security.MessageDigest
|
||||
import java.util.Base64
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
fun decrypt(responseData: String): String {
|
||||
val enc = responseData.split("|")
|
||||
val secretKey = enc[3].sha256().hexStringToByteArray()
|
||||
|
||||
return enc[0].aesDecrypt(secretKey, enc[2])
|
||||
}
|
||||
|
||||
private fun String.hexStringToByteArray(): ByteArray {
|
||||
val len = this.length
|
||||
val data = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
data[i / 2] = (
|
||||
(Character.digit(this[i], 16) shl 4) +
|
||||
Character.digit(this[i + 1], 16)
|
||||
).toByte()
|
||||
i += 2
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private fun String.sha256(): String {
|
||||
return MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest(this.toByteArray())
|
||||
.fold("", { str, it -> str + "%02x".format(it) })
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
|
||||
val decoder = Base64.getDecoder()
|
||||
val c = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val sk = SecretKeySpec(secretKey, "AES")
|
||||
val iv = IvParameterSpec(decoder.decode(ivString.toByteArray(Charsets.UTF_8)))
|
||||
c.init(Cipher.DECRYPT_MODE, sk, iv)
|
||||
|
||||
val byteStr = decoder.decode(this.toByteArray(Charsets.UTF_8))
|
||||
return String(c.doFinal(byteStr))
|
||||
}
|
|
@ -0,0 +1,334 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.github.salomonbrys.kotson.addAll
|
||||
import com.github.salomonbrys.kotson.addProperty
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonNull
|
||||
import com.google.gson.JsonObject
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import java.lang.Exception
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
class GmangaFilters() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getFilterList() = FilterList(
|
||||
MangaTypeFilter(),
|
||||
OneShotFilter(),
|
||||
StoryStatusFilter(),
|
||||
TranslationStatusFilter(),
|
||||
ChapterCountFilter(),
|
||||
DateRangeFilter(),
|
||||
CategoryFilter()
|
||||
)
|
||||
|
||||
fun buildSearchPayload(page: Int, query: String = "", filters: FilterList): JsonObject {
|
||||
val mangaTypeFilter = filters.findInstance<MangaTypeFilter>()!!
|
||||
val oneShotFilter = filters.findInstance<OneShotFilter>()!!
|
||||
val storyStatusFilter = filters.findInstance<StoryStatusFilter>()!!
|
||||
val translationStatusFilter = filters.findInstance<TranslationStatusFilter>()!!
|
||||
val chapterCountFilter = filters.findInstance<ChapterCountFilter>()!!
|
||||
val dateRangeFilter = filters.findInstance<DateRangeFilter>()!!
|
||||
val categoryFilter = filters.findInstance<CategoryFilter>()!!
|
||||
|
||||
return JsonObject().apply {
|
||||
|
||||
oneShotFilter.state.first().let {
|
||||
when {
|
||||
it.isIncluded() -> addProperty("oneshot", true)
|
||||
it.isExcluded() -> addProperty("oneshot", false)
|
||||
else -> addProperty("oneshot", JsonNull.INSTANCE)
|
||||
}
|
||||
}
|
||||
|
||||
addProperty("title", query)
|
||||
addProperty("page", page)
|
||||
addProperty(
|
||||
"manga_types",
|
||||
JsonObject().apply {
|
||||
|
||||
addProperty(
|
||||
"include",
|
||||
JsonArray().apply {
|
||||
addAll(mangaTypeFilter.state.filter { it.isIncluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
|
||||
addProperty(
|
||||
"exclude",
|
||||
JsonArray().apply {
|
||||
addAll(mangaTypeFilter.state.filter { it.isExcluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
addProperty(
|
||||
"story_status",
|
||||
JsonObject().apply {
|
||||
|
||||
addProperty(
|
||||
"include",
|
||||
JsonArray().apply {
|
||||
addAll(storyStatusFilter.state.filter { it.isIncluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
|
||||
addProperty(
|
||||
"exclude",
|
||||
JsonArray().apply {
|
||||
addAll(storyStatusFilter.state.filter { it.isExcluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
addProperty(
|
||||
"translation_status",
|
||||
JsonObject().apply {
|
||||
|
||||
addProperty(
|
||||
"include",
|
||||
JsonArray().apply {
|
||||
addAll(translationStatusFilter.state.filter { it.isIncluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
|
||||
addProperty(
|
||||
"exclude",
|
||||
JsonArray().apply {
|
||||
addAll(translationStatusFilter.state.filter { it.isExcluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
addProperty(
|
||||
"categories",
|
||||
JsonObject().apply {
|
||||
|
||||
addProperty(
|
||||
"include",
|
||||
JsonArray().apply {
|
||||
add(JsonNull.INSTANCE) // always included, maybe to avoid shifting index in the backend
|
||||
addAll(categoryFilter.state.filter { it.isIncluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
|
||||
addProperty(
|
||||
"exclude",
|
||||
JsonArray().apply {
|
||||
addAll(categoryFilter.state.filter { it.isExcluded() }.map { it.id })
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
addProperty(
|
||||
"chapters",
|
||||
JsonObject().apply {
|
||||
|
||||
addPropertyFromValidatingTextFilter(
|
||||
chapterCountFilter.state.first {
|
||||
it.id == FILTER_ID_MIN_CHAPTER_COUNT
|
||||
},
|
||||
"min",
|
||||
ERROR_INVALID_MIN_CHAPTER_COUNT,
|
||||
""
|
||||
)
|
||||
|
||||
addPropertyFromValidatingTextFilter(
|
||||
chapterCountFilter.state.first {
|
||||
it.id == FILTER_ID_MAX_CHAPTER_COUNT
|
||||
},
|
||||
"max",
|
||||
ERROR_INVALID_MAX_CHAPTER_COUNT,
|
||||
""
|
||||
)
|
||||
}
|
||||
)
|
||||
addProperty(
|
||||
"dates",
|
||||
JsonObject().apply {
|
||||
|
||||
addPropertyFromValidatingTextFilter(
|
||||
dateRangeFilter.state.first {
|
||||
it.id == FILTER_ID_START_DATE
|
||||
},
|
||||
"start",
|
||||
ERROR_INVALID_START_DATE
|
||||
)
|
||||
|
||||
addPropertyFromValidatingTextFilter(
|
||||
dateRangeFilter.state.first {
|
||||
it.id == FILTER_ID_END_DATE
|
||||
},
|
||||
"end",
|
||||
ERROR_INVALID_END_DATE
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// filter IDs
|
||||
private const val FILTER_ID_ONE_SHOT = "oneshot"
|
||||
private const val FILTER_ID_START_DATE = "start"
|
||||
private const val FILTER_ID_END_DATE = "end"
|
||||
private const val FILTER_ID_MIN_CHAPTER_COUNT = "min"
|
||||
private const val FILTER_ID_MAX_CHAPTER_COUNT = "max"
|
||||
|
||||
// error messages
|
||||
private const val ERROR_INVALID_START_DATE = "تاريخ بداية غير صالح"
|
||||
private const val ERROR_INVALID_END_DATE = " تاريخ نهاية غير صالح"
|
||||
private const val ERROR_INVALID_MIN_CHAPTER_COUNT = "الحد الأدنى لعدد الفصول غير صالح"
|
||||
private const val ERROR_INVALID_MAX_CHAPTER_COUNT = "الحد الأقصى لعدد الفصول غير صالح"
|
||||
|
||||
private class MangaTypeFilter() : Filter.Group<TagFilter>(
|
||||
"الأصل",
|
||||
listOf(
|
||||
TagFilter("1", "يابانية", TriState.STATE_INCLUDE),
|
||||
TagFilter("2", "كورية", TriState.STATE_INCLUDE),
|
||||
TagFilter("3", "صينية", TriState.STATE_INCLUDE),
|
||||
TagFilter("4", "عربية", TriState.STATE_INCLUDE),
|
||||
TagFilter("5", "كوميك", TriState.STATE_INCLUDE),
|
||||
TagFilter("6", "هواة", TriState.STATE_INCLUDE),
|
||||
TagFilter("7", "إندونيسية", TriState.STATE_INCLUDE),
|
||||
TagFilter("8", "روسية", TriState.STATE_INCLUDE),
|
||||
)
|
||||
)
|
||||
|
||||
private class OneShotFilter() : Filter.Group<TagFilter>(
|
||||
"ونشوت؟",
|
||||
listOf(
|
||||
TagFilter(FILTER_ID_ONE_SHOT, "نعم", TriState.STATE_EXCLUDE)
|
||||
)
|
||||
)
|
||||
|
||||
private class StoryStatusFilter() : Filter.Group<TagFilter>(
|
||||
"حالة القصة",
|
||||
listOf(
|
||||
TagFilter("2", "مستمرة"),
|
||||
TagFilter("3", "منتهية")
|
||||
)
|
||||
)
|
||||
|
||||
private class TranslationStatusFilter() : Filter.Group<TagFilter>(
|
||||
"حالة الترجمة",
|
||||
listOf(
|
||||
TagFilter("0", "منتهية"),
|
||||
TagFilter("1", "مستمرة"),
|
||||
TagFilter("2", "متوقفة"),
|
||||
TagFilter("3", "غير مترجمة", TriState.STATE_EXCLUDE),
|
||||
)
|
||||
)
|
||||
|
||||
private class ChapterCountFilter() : Filter.Group<IntFilter>(
|
||||
"عدد الفصول",
|
||||
listOf(
|
||||
IntFilter(FILTER_ID_MIN_CHAPTER_COUNT, "على الأقل"),
|
||||
IntFilter(FILTER_ID_MAX_CHAPTER_COUNT, "على الأكثر")
|
||||
)
|
||||
)
|
||||
|
||||
private class DateRangeFilter() : Filter.Group<DateFilter>(
|
||||
"تاريخ النشر",
|
||||
listOf(
|
||||
DateFilter(FILTER_ID_START_DATE, "تاريخ النشر"),
|
||||
DateFilter(FILTER_ID_END_DATE, "تاريخ الإنتهاء")
|
||||
)
|
||||
)
|
||||
|
||||
private class CategoryFilter() : Filter.Group<TagFilter>(
|
||||
"التصنيفات",
|
||||
listOf(
|
||||
TagFilter("1", "إثارة"),
|
||||
TagFilter("2", "أكشن"),
|
||||
TagFilter("3", "الحياة المدرسية"),
|
||||
TagFilter("4", "الحياة اليومية"),
|
||||
TagFilter("5", "آليات"),
|
||||
TagFilter("6", "تاريخي"),
|
||||
TagFilter("7", "تراجيدي"),
|
||||
TagFilter("8", "جوسيه"),
|
||||
TagFilter("9", "حربي"),
|
||||
TagFilter("10", "خيال"),
|
||||
TagFilter("11", "خيال علمي"),
|
||||
TagFilter("12", "دراما"),
|
||||
TagFilter("13", "رعب"),
|
||||
TagFilter("14", "رومانسي"),
|
||||
TagFilter("15", "رياضة"),
|
||||
TagFilter("16", "ساموراي"),
|
||||
TagFilter("17", "سحر"),
|
||||
TagFilter("18", "سينين"),
|
||||
TagFilter("19", "شوجو"),
|
||||
TagFilter("20", "شونين"),
|
||||
TagFilter("21", "عنف"),
|
||||
TagFilter("22", "غموض"),
|
||||
TagFilter("23", "فنون قتال"),
|
||||
TagFilter("24", "قوى خارقة"),
|
||||
TagFilter("25", "كوميدي"),
|
||||
TagFilter("26", "لعبة"),
|
||||
TagFilter("27", "مسابقة"),
|
||||
TagFilter("28", "مصاصي الدماء"),
|
||||
TagFilter("29", "مغامرات"),
|
||||
TagFilter("30", "موسيقى"),
|
||||
TagFilter("31", "نفسي"),
|
||||
TagFilter("32", "نينجا"),
|
||||
TagFilter("33", "وحوش"),
|
||||
TagFilter("34", "حريم"),
|
||||
TagFilter("35", "راشد"),
|
||||
TagFilter("38", "ويب-تون"),
|
||||
TagFilter("39", "زمنكاني")
|
||||
)
|
||||
)
|
||||
|
||||
private const val DATE_FILTER_PATTERN = "yyyy/MM/dd"
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private val DATE_FITLER_FORMAT = SimpleDateFormat(DATE_FILTER_PATTERN).apply {
|
||||
isLenient = false
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.isValid(date: String): Boolean {
|
||||
return try {
|
||||
this.parse(date)
|
||||
true
|
||||
} catch (e: ParseException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonObject.addPropertyFromValidatingTextFilter(
|
||||
filter: ValidatingTextFilter,
|
||||
property: String,
|
||||
invalidErrorMessage: String,
|
||||
default: String? = null
|
||||
) {
|
||||
filter.let {
|
||||
when {
|
||||
it.state == "" -> if (default == null) {
|
||||
addProperty(property, JsonNull.INSTANCE)
|
||||
} else addProperty(property, default)
|
||||
it.isValid() -> addProperty(property, it.state)
|
||||
else -> throw Exception(invalidErrorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
private class TagFilter(val id: String, name: String, state: Int = STATE_IGNORE) : Filter.TriState(name, state)
|
||||
|
||||
private abstract class ValidatingTextFilter(name: String) : Filter.Text(name) {
|
||||
abstract fun isValid(): Boolean
|
||||
}
|
||||
|
||||
private class DateFilter(val id: String, name: String) : ValidatingTextFilter("($DATE_FILTER_PATTERN) $name)") {
|
||||
override fun isValid(): Boolean = DATE_FITLER_FORMAT.isValid(this.state)
|
||||
}
|
||||
|
||||
private class IntFilter(val id: String, name: String) : ValidatingTextFilter(name) {
|
||||
override fun isValid(): Boolean = state.toIntOrNull() != null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.support.v7.preference.ListPreference
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class GmangaPreferences(id: Long) {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
|
||||
STRING_PREFERENCES.forEach {
|
||||
val preference = ListPreference(screen.context).apply {
|
||||
key = it.key
|
||||
title = it.title
|
||||
entries = it.entries()
|
||||
entryValues = it.entryValues()
|
||||
summary = "%s"
|
||||
}
|
||||
|
||||
if (!preferences.contains(it.key))
|
||||
preferences.edit().putString(it.key, it.default().key).apply()
|
||||
|
||||
screen.addPreference(preference)
|
||||
}
|
||||
}
|
||||
|
||||
fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
|
||||
STRING_PREFERENCES.forEach {
|
||||
val preference = androidx.preference.ListPreference(screen.context).apply {
|
||||
key = it.key
|
||||
title = it.title
|
||||
entries = it.entries()
|
||||
entryValues = it.entryValues()
|
||||
summary = "%s"
|
||||
}
|
||||
|
||||
if (!preferences.contains(it.key))
|
||||
preferences.edit().putString(it.key, it.default().key).apply()
|
||||
|
||||
screen.addPreference(preference)
|
||||
}
|
||||
}
|
||||
|
||||
fun getString(pref: StringPreference): String {
|
||||
return preferences.getString(pref.key, pref.default().key)!!
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
class StringPreferenceOption(val key: String, val title: String)
|
||||
|
||||
class StringPreference(
|
||||
val key: String,
|
||||
val title: String,
|
||||
private val options: List<StringPreferenceOption>,
|
||||
private val defaultOptionIndex: Int = 0
|
||||
) {
|
||||
fun entries(): Array<String> = options.map { it.title }.toTypedArray()
|
||||
fun entryValues(): Array<String> = options.map { it.key }.toTypedArray()
|
||||
fun default(): StringPreferenceOption = options[defaultOptionIndex]
|
||||
}
|
||||
|
||||
// preferences
|
||||
const val PREF_CHAPTER_LISTING_SHOW_ALL = "gmanga_gmanga_chapter_listing_show_all"
|
||||
const val PREF_CHAPTER_LISTING_SHOW_POPULAR = "gmanga_chapter_listing_most_viewed"
|
||||
|
||||
val PREF_CHAPTER_LISTING = StringPreference(
|
||||
"gmanga_chapter_listing",
|
||||
"كيفية عرض الفصل بقائمة الفصول",
|
||||
listOf(
|
||||
StringPreferenceOption(PREF_CHAPTER_LISTING_SHOW_POPULAR, "اختيار النسخة الأكثر مشاهدة"),
|
||||
StringPreferenceOption(PREF_CHAPTER_LISTING_SHOW_ALL, "عرض جميع النسخ")
|
||||
)
|
||||
)
|
||||
|
||||
private val STRING_PREFERENCES = listOf(
|
||||
PREF_CHAPTER_LISTING
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue