diff --git a/src/ar/gmanga/AndroidManifest.xml b/src/ar/gmanga/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/ar/gmanga/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/ar/gmanga/build.gradle b/src/ar/gmanga/build.gradle
new file mode 100644
index 000000000..fa9df6586
--- /dev/null
+++ b/src/ar/gmanga/build.gradle
@@ -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"
diff --git a/src/ar/gmanga/res/mipmap-hdpi/ic_launcher.png b/src/ar/gmanga/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a27c4d8a6
Binary files /dev/null and b/src/ar/gmanga/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/ar/gmanga/res/mipmap-mdpi/ic_launcher.png b/src/ar/gmanga/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..90b5adf95
Binary files /dev/null and b/src/ar/gmanga/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/ar/gmanga/res/mipmap-xhdpi/ic_launcher.png b/src/ar/gmanga/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..49d5b42dc
Binary files /dev/null and b/src/ar/gmanga/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/ar/gmanga/res/mipmap-xxhdpi/ic_launcher.png b/src/ar/gmanga/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..1283009d9
Binary files /dev/null and b/src/ar/gmanga/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/ar/gmanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/ar/gmanga/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f17cd73d4
Binary files /dev/null and b/src/ar/gmanga/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/ar/gmanga/res/web_hi_res_512.png b/src/ar/gmanga/res/web_hi_res_512.png
new file mode 100644
index 000000000..1d0cff9dd
Binary files /dev/null and b/src/ar/gmanga/res/web_hi_res_512.png differ
diff --git a/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/Gmanga.kt b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/Gmanga.kt
new file mode 100644
index 000000000..8e821b1c9
--- /dev/null
+++ b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/Gmanga.kt
@@ -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 {
+ val data = decryptResponse(response)
+
+ val chapters: List = 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) -> 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(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(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 {
+ val url = response.request().url().toString()
+ val data = gson.fromJson(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(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")
+ }
+}
diff --git a/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaCryptoUtils.kt b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaCryptoUtils.kt
new file mode 100644
index 000000000..511465fe3
--- /dev/null
+++ b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaCryptoUtils.kt
@@ -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))
+}
diff --git a/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaFilters.kt b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaFilters.kt
new file mode 100644
index 000000000..6c816697c
--- /dev/null
+++ b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaFilters.kt
@@ -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()!!
+ val oneShotFilter = filters.findInstance()!!
+ val storyStatusFilter = filters.findInstance()!!
+ val translationStatusFilter = filters.findInstance()!!
+ val chapterCountFilter = filters.findInstance()!!
+ val dateRangeFilter = filters.findInstance()!!
+ val categoryFilter = filters.findInstance()!!
+
+ 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(
+ "الأصل",
+ 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(
+ "ونشوت؟",
+ listOf(
+ TagFilter(FILTER_ID_ONE_SHOT, "نعم", TriState.STATE_EXCLUDE)
+ )
+ )
+
+ private class StoryStatusFilter() : Filter.Group(
+ "حالة القصة",
+ listOf(
+ TagFilter("2", "مستمرة"),
+ TagFilter("3", "منتهية")
+ )
+ )
+
+ private class TranslationStatusFilter() : Filter.Group(
+ "حالة الترجمة",
+ listOf(
+ TagFilter("0", "منتهية"),
+ TagFilter("1", "مستمرة"),
+ TagFilter("2", "متوقفة"),
+ TagFilter("3", "غير مترجمة", TriState.STATE_EXCLUDE),
+ )
+ )
+
+ private class ChapterCountFilter() : Filter.Group(
+ "عدد الفصول",
+ listOf(
+ IntFilter(FILTER_ID_MIN_CHAPTER_COUNT, "على الأقل"),
+ IntFilter(FILTER_ID_MAX_CHAPTER_COUNT, "على الأكثر")
+ )
+ )
+
+ private class DateRangeFilter() : Filter.Group(
+ "تاريخ النشر",
+ listOf(
+ DateFilter(FILTER_ID_START_DATE, "تاريخ النشر"),
+ DateFilter(FILTER_ID_END_DATE, "تاريخ الإنتهاء")
+ )
+ )
+
+ private class CategoryFilter() : Filter.Group(
+ "التصنيفات",
+ 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 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
+ }
+ }
+}
diff --git a/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaPreferences.kt b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaPreferences.kt
new file mode 100644
index 000000000..76af1c357
--- /dev/null
+++ b/src/ar/gmanga/src/eu/kanade/tachiyomi/extension/ar/gmanga/GmangaPreferences.kt
@@ -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().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,
+ private val defaultOptionIndex: Int = 0
+ ) {
+ fun entries(): Array = options.map { it.title }.toTypedArray()
+ fun entryValues(): Array = 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
+ )
+ }
+}