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 + ) + } +}