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