Gmanga multisrc: Add Dilar & MangaTales (#1767)
* gmanga multisrc * search payload and filters refactor * ratelimit * distinct * dynamic filters * dilar * gmanga multisrc: latest * gmanga multisrc: search & filter * gmanga multisrc: chapters & pages * small cleanup * remove obsolete preferences * small cleanup & arabic tl deepl * Dilar: filter paid chapters * GManga: use unencrypted alt api for chapters * abstract away sort of chapters and pages * remove chapters logic from multisrc class since all three have different logic * remove `this`
							
								
								
									
										5
									
								
								lib-multisrc/gmanga/build.gradle.kts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					plugins {
 | 
				
			||||||
 | 
					    id("lib-multisrc")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					baseVersionCode = 1
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
 | 
					package eu.kanade.tachiyomi.multisrc.gmanga
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import android.util.Base64
 | 
					import android.util.Base64
 | 
				
			||||||
import java.security.MessageDigest
 | 
					import java.security.MessageDigest
 | 
				
			||||||
@ -14,7 +14,7 @@ fun decrypt(responseData: String): String {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private fun String.hexStringToByteArray(): ByteArray {
 | 
					private fun String.hexStringToByteArray(): ByteArray {
 | 
				
			||||||
    val len = this.length
 | 
					    val len = length
 | 
				
			||||||
    val data = ByteArray(len / 2)
 | 
					    val data = ByteArray(len / 2)
 | 
				
			||||||
    var i = 0
 | 
					    var i = 0
 | 
				
			||||||
    while (i < len) {
 | 
					    while (i < len) {
 | 
				
			||||||
@ -30,8 +30,8 @@ private fun String.hexStringToByteArray(): ByteArray {
 | 
				
			|||||||
private fun String.sha256(): String {
 | 
					private fun String.sha256(): String {
 | 
				
			||||||
    return MessageDigest
 | 
					    return MessageDigest
 | 
				
			||||||
        .getInstance("SHA-256")
 | 
					        .getInstance("SHA-256")
 | 
				
			||||||
        .digest(this.toByteArray())
 | 
					        .digest(toByteArray())
 | 
				
			||||||
        .fold("", { str, it -> str + "%02x".format(it) })
 | 
					        .fold("") { str, it -> str + "%02x".format(it) }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
 | 
					private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
 | 
				
			||||||
@ -40,6 +40,6 @@ private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
 | 
				
			|||||||
    val iv = IvParameterSpec(Base64.decode(ivString.toByteArray(Charsets.UTF_8), Base64.DEFAULT))
 | 
					    val iv = IvParameterSpec(Base64.decode(ivString.toByteArray(Charsets.UTF_8), Base64.DEFAULT))
 | 
				
			||||||
    c.init(Cipher.DECRYPT_MODE, sk, iv)
 | 
					    c.init(Cipher.DECRYPT_MODE, sk, iv)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val byteStr = Base64.decode(this.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
 | 
					    val byteStr = Base64.decode(toByteArray(Charsets.UTF_8), Base64.DEFAULT)
 | 
				
			||||||
    return String(c.doFinal(byteStr))
 | 
					    return String(c.doFinal(byteStr))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -0,0 +1,147 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.multisrc.gmanga
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.SManga
 | 
				
			||||||
 | 
					import kotlinx.serialization.SerialName
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class EncryptedResponse(val data: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class MangaDataAction<T>(val mangaDataAction: T)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class LatestChaptersDto(
 | 
				
			||||||
 | 
					    val releases: List<LatestReleaseDto>,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class LatestReleaseDto(
 | 
				
			||||||
 | 
					    val manga: BrowseManga,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class SearchMangaDto(
 | 
				
			||||||
 | 
					    val mangas: List<BrowseManga>,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class BrowseManga(
 | 
				
			||||||
 | 
					    private val id: Int,
 | 
				
			||||||
 | 
					    private val title: String,
 | 
				
			||||||
 | 
					    private val cover: String,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
 | 
				
			||||||
 | 
					        url = "/mangas/$id"
 | 
				
			||||||
 | 
					        title = this@BrowseManga.title
 | 
				
			||||||
 | 
					        thumbnail_url = createThumbnail(id.toString(), cover)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class FiltersDto(
 | 
				
			||||||
 | 
					    val categoryTypes: List<FiltersDto>? = null,
 | 
				
			||||||
 | 
					    val categories: List<FilterDto>? = null,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class FilterDto(
 | 
				
			||||||
 | 
					    val name: String,
 | 
				
			||||||
 | 
					    val id: Int,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class MangaDetailsDto(
 | 
				
			||||||
 | 
					    val mangaData: Manga,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class Manga(
 | 
				
			||||||
 | 
					    private val id: Int,
 | 
				
			||||||
 | 
					    private val cover: String,
 | 
				
			||||||
 | 
					    private val title: String,
 | 
				
			||||||
 | 
					    private val summary: String? = null,
 | 
				
			||||||
 | 
					    private val artists: List<NameDto>,
 | 
				
			||||||
 | 
					    private val authors: List<NameDto>,
 | 
				
			||||||
 | 
					    @SerialName("story_status") private val status: Int,
 | 
				
			||||||
 | 
					    private val type: TypeDto,
 | 
				
			||||||
 | 
					    private val categories: List<NameDto>,
 | 
				
			||||||
 | 
					    @SerialName("translation_status") private val tlStatus: Int,
 | 
				
			||||||
 | 
					    private val synonyms: String? = null,
 | 
				
			||||||
 | 
					    @SerialName("arabic_title") private val arTitle: String? = null,
 | 
				
			||||||
 | 
					    @SerialName("japanese") private val jpTitle: String? = null,
 | 
				
			||||||
 | 
					    @SerialName("english") private val enTitle: String? = null,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    fun toSManga(createThumbnail: (String, String) -> String) = SManga.create().apply {
 | 
				
			||||||
 | 
					        title = this@Manga.title
 | 
				
			||||||
 | 
					        thumbnail_url = createThumbnail(id.toString(), cover)
 | 
				
			||||||
 | 
					        artist = artists.joinToString { it.name }
 | 
				
			||||||
 | 
					        author = authors.joinToString { it.name }
 | 
				
			||||||
 | 
					        status = when (this@Manga.status) {
 | 
				
			||||||
 | 
					            2 -> SManga.ONGOING
 | 
				
			||||||
 | 
					            3 -> SManga.COMPLETED
 | 
				
			||||||
 | 
					            else -> SManga.UNKNOWN
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        genre = buildList {
 | 
				
			||||||
 | 
					            add(type.title)
 | 
				
			||||||
 | 
					            add(type.name)
 | 
				
			||||||
 | 
					            categories.forEach { add(it.name) }
 | 
				
			||||||
 | 
					        }.joinToString()
 | 
				
			||||||
 | 
					        description = buildString {
 | 
				
			||||||
 | 
					            summary.orEmpty()
 | 
				
			||||||
 | 
					                .ifEmpty { "لم يتم اضافة قصة بعد" }
 | 
				
			||||||
 | 
					                .also { append(it) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            when (tlStatus) {
 | 
				
			||||||
 | 
					                0 -> "منتهية"
 | 
				
			||||||
 | 
					                1 -> "مستمرة"
 | 
				
			||||||
 | 
					                2 -> "متوقفة"
 | 
				
			||||||
 | 
					                else -> "مجهول"
 | 
				
			||||||
 | 
					            }.also {
 | 
				
			||||||
 | 
					                append("\n\n")
 | 
				
			||||||
 | 
					                append("حالة الترجمة")
 | 
				
			||||||
 | 
					                append(":\n• ")
 | 
				
			||||||
 | 
					                append(it)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            val titles = listOfNotNull(synonyms, arTitle, jpTitle, enTitle)
 | 
				
			||||||
 | 
					            if (titles.isNotEmpty()) {
 | 
				
			||||||
 | 
					                append("\n\n")
 | 
				
			||||||
 | 
					                append("مسميّات أخرى")
 | 
				
			||||||
 | 
					                append(":\n• ")
 | 
				
			||||||
 | 
					                append(titles.joinToString("\n• "))
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class NameDto(val name: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class TypeDto(
 | 
				
			||||||
 | 
					    val name: String,
 | 
				
			||||||
 | 
					    val title: String,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderDto(
 | 
				
			||||||
 | 
					    val readerDataAction: ReaderData,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderData(
 | 
				
			||||||
 | 
					    val readerData: ReaderChapter,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderChapter(
 | 
				
			||||||
 | 
					    val release: ReaderPages,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderPages(
 | 
				
			||||||
 | 
					    @SerialName("webp_pages") val webpPages: List<String>,
 | 
				
			||||||
 | 
					    val pages: List<String>,
 | 
				
			||||||
 | 
					    @SerialName("storage_key") val key: String,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -0,0 +1,94 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.multisrc.gmanga
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
 | 
					import java.text.ParseException
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TagFilterData(
 | 
				
			||||||
 | 
					    private val id: String,
 | 
				
			||||||
 | 
					    private val name: String,
 | 
				
			||||||
 | 
					    private val state: Int = Filter.TriState.STATE_IGNORE,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    fun toTagFilter() = TagFilter(id, name, state)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TagFilter(
 | 
				
			||||||
 | 
					    val id: String,
 | 
				
			||||||
 | 
					    name: String,
 | 
				
			||||||
 | 
					    state: Int = STATE_IGNORE,
 | 
				
			||||||
 | 
					) : Filter.TriState(name, state)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					abstract class ValidatingTextFilter(name: String) : Filter.Text(name) {
 | 
				
			||||||
 | 
					    abstract fun isValid(): Boolean
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private val DATE_FITLER_FORMAT = SimpleDateFormat("yyyy/MM/dd", Locale.ENGLISH).apply {
 | 
				
			||||||
 | 
					    isLenient = false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private fun SimpleDateFormat.isValid(date: String): Boolean {
 | 
				
			||||||
 | 
					    return try {
 | 
				
			||||||
 | 
					        parse(date)
 | 
				
			||||||
 | 
					        true
 | 
				
			||||||
 | 
					    } catch (e: ParseException) {
 | 
				
			||||||
 | 
					        false
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DateFilter(val id: String, name: String) : ValidatingTextFilter("(yyyy/MM/dd) $name)") {
 | 
				
			||||||
 | 
					    override fun isValid(): Boolean = DATE_FITLER_FORMAT.isValid(state)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class IntFilter(val id: String, name: String) : ValidatingTextFilter(name) {
 | 
				
			||||||
 | 
					    override fun isValid(): Boolean = state.toIntOrNull() != null
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MangaTypeFilter(types: List<TagFilterData>) : Filter.Group<TagFilter>(
 | 
				
			||||||
 | 
					    "الأصل",
 | 
				
			||||||
 | 
					    types.map { it.toTagFilter() },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OneShotFilter : Filter.Group<TagFilter>(
 | 
				
			||||||
 | 
					    "ونشوت؟",
 | 
				
			||||||
 | 
					    listOf(
 | 
				
			||||||
 | 
					        TagFilter("oneshot", "نعم", TriState.STATE_EXCLUDE),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class StoryStatusFilter(status: List<TagFilterData>) : Filter.Group<TagFilter>(
 | 
				
			||||||
 | 
					    "حالة القصة",
 | 
				
			||||||
 | 
					    status.map { it.toTagFilter() },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TranslationStatusFilter(tlStatus: List<TagFilterData>) : Filter.Group<TagFilter>(
 | 
				
			||||||
 | 
					    "حالة الترجمة",
 | 
				
			||||||
 | 
					    tlStatus.map { it.toTagFilter() },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ChapterCountFilter : Filter.Group<IntFilter>(
 | 
				
			||||||
 | 
					    "عدد الفصول",
 | 
				
			||||||
 | 
					    listOf(
 | 
				
			||||||
 | 
					        IntFilter("min", "على الأقل"),
 | 
				
			||||||
 | 
					        IntFilter("max", "على الأكثر"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    val min get() = state.first { it.id == "min" }
 | 
				
			||||||
 | 
					    val max get() = state.first { it.id == "max" }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class DateRangeFilter : Filter.Group<DateFilter>(
 | 
				
			||||||
 | 
					    "تاريخ النشر",
 | 
				
			||||||
 | 
					    listOf(
 | 
				
			||||||
 | 
					        DateFilter("start", "تاريخ النشر"),
 | 
				
			||||||
 | 
					        DateFilter("end", "تاريخ الإنتهاء"),
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    val start get() = state.first { it.id == "start" }
 | 
				
			||||||
 | 
					    val end get() = state.first { it.id == "end" }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CategoryFilter(categories: List<TagFilterData>) : Filter.Group<TagFilter>(
 | 
				
			||||||
 | 
					    "التصنيفات",
 | 
				
			||||||
 | 
					    categories.map { it.toTagFilter() },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -0,0 +1,297 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.multisrc.gmanga
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import android.util.Log
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.POST
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.await
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.FilterList
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.MangasPage
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.Page
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.SChapter
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.SManga
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.online.HttpSource
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.util.asJsoup
 | 
				
			||||||
 | 
					import kotlinx.coroutines.CoroutineScope
 | 
				
			||||||
 | 
					import kotlinx.coroutines.Dispatchers
 | 
				
			||||||
 | 
					import kotlinx.coroutines.launch
 | 
				
			||||||
 | 
					import kotlinx.serialization.decodeFromString
 | 
				
			||||||
 | 
					import kotlinx.serialization.encodeToString
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.Json
 | 
				
			||||||
 | 
					import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
				
			||||||
 | 
					import okhttp3.Request
 | 
				
			||||||
 | 
					import okhttp3.RequestBody.Companion.toRequestBody
 | 
				
			||||||
 | 
					import okhttp3.Response
 | 
				
			||||||
 | 
					import uy.kohesive.injekt.injectLazy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					abstract class Gmanga(
 | 
				
			||||||
 | 
					    override val name: String,
 | 
				
			||||||
 | 
					    override val baseUrl: String,
 | 
				
			||||||
 | 
					    final override val lang: String,
 | 
				
			||||||
 | 
					    protected val cdnUrl: String = baseUrl,
 | 
				
			||||||
 | 
					) : HttpSource() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val supportsLatest = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected val json: Json by injectLazy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override val client = network.cloudflareClient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun headersBuilder() = super.headersBuilder()
 | 
				
			||||||
 | 
					        .set("Referer", "$baseUrl/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
 | 
				
			||||||
 | 
					    override fun popularMangaParse(response: Response) = searchMangaParse(response)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun latestUpdatesRequest(page: Int): Request {
 | 
				
			||||||
 | 
					        return GET("$baseUrl/api/releases?page=$page", headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
				
			||||||
 | 
					        val releases = response.parseAs<LatestChaptersDto>().releases
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val entries = releases.map { it.manga.toSManga(::createThumbnail) }
 | 
				
			||||||
 | 
					            .distinctBy { it.url }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return MangasPage(
 | 
				
			||||||
 | 
					            entries,
 | 
				
			||||||
 | 
					            hasNextPage = (releases.size >= 30),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
				
			||||||
 | 
					        val filterList = if (filters.isEmpty()) getFilterList() else filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val mangaTypeFilter = filterList.findInstance<MangaTypeFilter>()!!
 | 
				
			||||||
 | 
					        val oneShotFilter = filterList.findInstance<OneShotFilter>()!!
 | 
				
			||||||
 | 
					        val storyStatusFilter = filterList.findInstance<StoryStatusFilter>()!!
 | 
				
			||||||
 | 
					        val translationStatusFilter = filterList.findInstance<TranslationStatusFilter>()!!
 | 
				
			||||||
 | 
					        val chapterCountFilter = filterList.findInstance<ChapterCountFilter>()!!
 | 
				
			||||||
 | 
					        val dateRangeFilter = filterList.findInstance<DateRangeFilter>()!!
 | 
				
			||||||
 | 
					        val categoryFilter = filterList.findInstance<CategoryFilter>() ?: CategoryFilter(emptyList())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val body = SearchPayload(
 | 
				
			||||||
 | 
					            oneshot = OneShot(
 | 
				
			||||||
 | 
					                value = oneShotFilter.state.first().run {
 | 
				
			||||||
 | 
					                    when {
 | 
				
			||||||
 | 
					                        isIncluded() -> true
 | 
				
			||||||
 | 
					                        else -> false
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            title = query,
 | 
				
			||||||
 | 
					            page = page,
 | 
				
			||||||
 | 
					            mangaTypes = IncludeExclude(
 | 
				
			||||||
 | 
					                include = mangaTypeFilter.state.filter { it.isIncluded() }.map { it.id },
 | 
				
			||||||
 | 
					                exclude = mangaTypeFilter.state.filter { it.isExcluded() }.map { it.id },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            storyStatus = IncludeExclude(
 | 
				
			||||||
 | 
					                include = storyStatusFilter.state.filter { it.isIncluded() }.map { it.id },
 | 
				
			||||||
 | 
					                exclude = storyStatusFilter.state.filter { it.isExcluded() }.map { it.id },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            tlStatus = IncludeExclude(
 | 
				
			||||||
 | 
					                include = translationStatusFilter.state.filter { it.isIncluded() }.map { it.id },
 | 
				
			||||||
 | 
					                exclude = translationStatusFilter.state.filter { it.isExcluded() }.map { it.id },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            categories = IncludeExclude(
 | 
				
			||||||
 | 
					                // always include null, maybe to avoid shifting index in the backend
 | 
				
			||||||
 | 
					                include = listOf(null) + categoryFilter.state.filter { it.isIncluded() }.map { it.id },
 | 
				
			||||||
 | 
					                exclude = categoryFilter.state.filter { it.isExcluded() }.map { it.id },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            chapters = MinMax(
 | 
				
			||||||
 | 
					                min = chapterCountFilter.min.run {
 | 
				
			||||||
 | 
					                    when {
 | 
				
			||||||
 | 
					                        state == "" -> ""
 | 
				
			||||||
 | 
					                        isValid() -> state
 | 
				
			||||||
 | 
					                        else -> throw Exception("الحد الأدنى لعدد الفصول غير صالح")
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                max = chapterCountFilter.max.run {
 | 
				
			||||||
 | 
					                    when {
 | 
				
			||||||
 | 
					                        state == "" -> ""
 | 
				
			||||||
 | 
					                        isValid() -> state
 | 
				
			||||||
 | 
					                        else -> throw Exception("الحد الأقصى لعدد الفصول غير صالح")
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            dates = StartEnd(
 | 
				
			||||||
 | 
					                start = dateRangeFilter.start.run {
 | 
				
			||||||
 | 
					                    when {
 | 
				
			||||||
 | 
					                        state == "" -> ""
 | 
				
			||||||
 | 
					                        isValid() -> state
 | 
				
			||||||
 | 
					                        else -> throw Exception("تاريخ بداية غير صالح")
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                end = dateRangeFilter.end.run {
 | 
				
			||||||
 | 
					                    when {
 | 
				
			||||||
 | 
					                        state == "" -> ""
 | 
				
			||||||
 | 
					                        isValid() -> state
 | 
				
			||||||
 | 
					                        else -> throw Exception("تاريخ نهاية غير صالح")
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ).let(json::encodeToString).toRequestBody(MEDIA_TYPE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return POST("$baseUrl/api/mangas/search", headers, body)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private var categories: List<TagFilterData> = emptyList()
 | 
				
			||||||
 | 
					    private var filtersState = FilterState.Unfetched
 | 
				
			||||||
 | 
					    private var filterAttempts = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private enum class FilterState {
 | 
				
			||||||
 | 
					        Fetching, Fetched, Unfetched
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private suspend fun fetchFilters() {
 | 
				
			||||||
 | 
					        if (filtersState == FilterState.Unfetched && filterAttempts < 3) {
 | 
				
			||||||
 | 
					            filtersState = FilterState.Fetching
 | 
				
			||||||
 | 
					            filterAttempts++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					                categories = client.newCall(GET("$baseUrl/mangas/", headers))
 | 
				
			||||||
 | 
					                    .await()
 | 
				
			||||||
 | 
					                    .asJsoup()
 | 
				
			||||||
 | 
					                    .select(".js-react-on-rails-component").html()
 | 
				
			||||||
 | 
					                    .parseAs<FiltersDto>()
 | 
				
			||||||
 | 
					                    .run {
 | 
				
			||||||
 | 
					                        categories ?: categoryTypes!!.flatMap { it.categories!! }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    .map { TagFilterData(it.id.toString(), it.name) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                filtersState = FilterState.Fetched
 | 
				
			||||||
 | 
					            } catch (e: Exception) {
 | 
				
			||||||
 | 
					                Log.e(name, e.stackTraceToString())
 | 
				
			||||||
 | 
					                filtersState = FilterState.Unfetched
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected open fun getTypesFilter() = listOf(
 | 
				
			||||||
 | 
					        TagFilterData("1", "يابانية", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("2", "كورية", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("3", "صينية", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("4", "عربية", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("5", "كوميك", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("6", "هواة", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("7", "إندونيسية", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("8", "روسية", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected open fun getStatusFilter() = listOf(
 | 
				
			||||||
 | 
					        TagFilterData("2", "مستمرة"),
 | 
				
			||||||
 | 
					        TagFilterData("3", "منتهية"),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected open fun getTranslationFilter() = listOf(
 | 
				
			||||||
 | 
					        TagFilterData("0", "منتهية"),
 | 
				
			||||||
 | 
					        TagFilterData("1", "مستمرة"),
 | 
				
			||||||
 | 
					        TagFilterData("2", "متوقفة"),
 | 
				
			||||||
 | 
					        TagFilterData("3", "غير مترجمة", Filter.TriState.STATE_EXCLUDE),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getFilterList(): FilterList {
 | 
				
			||||||
 | 
					        CoroutineScope(Dispatchers.IO).launch { fetchFilters() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val filters = mutableListOf<Filter<*>>(
 | 
				
			||||||
 | 
					            MangaTypeFilter(getTypesFilter()),
 | 
				
			||||||
 | 
					            OneShotFilter(),
 | 
				
			||||||
 | 
					            StoryStatusFilter(getStatusFilter()),
 | 
				
			||||||
 | 
					            TranslationStatusFilter(getTranslationFilter()),
 | 
				
			||||||
 | 
					            ChapterCountFilter(),
 | 
				
			||||||
 | 
					            DateRangeFilter(),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        filters += if (filtersState == FilterState.Fetched) {
 | 
				
			||||||
 | 
					            listOf(
 | 
				
			||||||
 | 
					                CategoryFilter(categories),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            listOf(
 | 
				
			||||||
 | 
					                Filter.Separator(),
 | 
				
			||||||
 | 
					                Filter.Header("اضغط على\"إعادة تعيين\"لمحاولة تحميل التصنيفات"),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return FilterList(filters)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun searchMangaParse(response: Response): MangasPage {
 | 
				
			||||||
 | 
					        val data = response.decryptAs<SearchMangaDto>()
 | 
				
			||||||
 | 
					        return MangasPage(
 | 
				
			||||||
 | 
					            data.mangas.map { it.toSManga(::createThumbnail) },
 | 
				
			||||||
 | 
					            hasNextPage = data.mangas.size == 50,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun mangaDetailsParse(response: Response): SManga {
 | 
				
			||||||
 | 
					        return response.asJsoup()
 | 
				
			||||||
 | 
					            .select(".js-react-on-rails-component").html()
 | 
				
			||||||
 | 
					            .parseAs<MangaDataAction<MangaDetailsDto>>()
 | 
				
			||||||
 | 
					            .mangaDataAction.mangaData
 | 
				
			||||||
 | 
					            .toSManga(::createThumbnail)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    abstract fun chaptersRequest(manga: SManga): Request
 | 
				
			||||||
 | 
					    abstract fun chaptersParse(response: Response): List<SChapter>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final override fun chapterListRequest(manga: SManga) = chaptersRequest(manga)
 | 
				
			||||||
 | 
					    final override fun chapterListParse(response: Response) = chaptersParse(response).sortChapters()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun List<SChapter>.sortChapters() =
 | 
				
			||||||
 | 
					        sortedWith(
 | 
				
			||||||
 | 
					            compareBy(
 | 
				
			||||||
 | 
					                { -it.chapter_number },
 | 
				
			||||||
 | 
					                { -it.date_upload },
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun pageListParse(response: Response): List<Page> {
 | 
				
			||||||
 | 
					        val data = response.asJsoup()
 | 
				
			||||||
 | 
					            .select(".js-react-on-rails-component").html()
 | 
				
			||||||
 | 
					            .parseAs<ReaderDto>()
 | 
				
			||||||
 | 
					            .readerDataAction.readerData.release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val hasWebP = data.webpPages.isNotEmpty()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val (pages, directory) = when {
 | 
				
			||||||
 | 
					            hasWebP -> data.webpPages to "hq_webp"
 | 
				
			||||||
 | 
					            else -> data.pages to "hq"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return pages.sortedWith(pageSort).mapIndexed { index, pageUri ->
 | 
				
			||||||
 | 
					            Page(
 | 
				
			||||||
 | 
					                index = index,
 | 
				
			||||||
 | 
					                imageUrl = "$cdnUrl/uploads/releases/${data.key}/$directory/$pageUri",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private val pageSort =
 | 
				
			||||||
 | 
					        compareBy<String>({ parseNumber(0, it) ?: Double.MAX_VALUE }, { parseNumber(1, it) }, { parseNumber(2, it) })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private fun parseNumber(index: Int, string: String): Double? =
 | 
				
			||||||
 | 
					        Regex("\\d+").findAll(string).map { it.value }.toList().getOrNull(index)?.toDoubleOrNull()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected inline fun <reified T> Response.decryptAs(): T =
 | 
				
			||||||
 | 
					        decrypt(parseAs<EncryptedResponse>().data).parseAs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected inline fun <reified T> Response.parseAs(): T = body.string().parseAs()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected inline fun <reified T> String.parseAs(): T = json.decodeFromString(this)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected open fun createThumbnail(mangaId: String, cover: String): String {
 | 
				
			||||||
 | 
					        val thumbnail = "large_${cover.substringBeforeLast(".")}.webp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return "$cdnUrl/uploads/manga/cover/$mangaId/$thumbnail"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun imageUrlParse(response: Response): String =
 | 
				
			||||||
 | 
					        throw UnsupportedOperationException()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    companion object {
 | 
				
			||||||
 | 
					        private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.multisrc.gmanga
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.SerialName
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class SearchPayload(
 | 
				
			||||||
 | 
					    private val oneshot: OneShot,
 | 
				
			||||||
 | 
					    private val title: String,
 | 
				
			||||||
 | 
					    private val page: Int,
 | 
				
			||||||
 | 
					    @SerialName("manga_types") private val mangaTypes: IncludeExclude,
 | 
				
			||||||
 | 
					    @SerialName("story_status") private val storyStatus: IncludeExclude,
 | 
				
			||||||
 | 
					    @SerialName("translation_status") val tlStatus: IncludeExclude,
 | 
				
			||||||
 | 
					    private val categories: IncludeExclude,
 | 
				
			||||||
 | 
					    private val chapters: MinMax,
 | 
				
			||||||
 | 
					    private val dates: StartEnd,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class OneShot(
 | 
				
			||||||
 | 
					    private val value: Boolean,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class IncludeExclude(
 | 
				
			||||||
 | 
					    private val include: List<String?>,
 | 
				
			||||||
 | 
					    private val exclude: List<String?>,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class MinMax(
 | 
				
			||||||
 | 
					    private val min: String,
 | 
				
			||||||
 | 
					    private val max: String,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class StartEnd(
 | 
				
			||||||
 | 
					    private val start: String,
 | 
				
			||||||
 | 
					    private val end: String,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/ar/dilar/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					ext {
 | 
				
			||||||
 | 
					    extName = 'Dilar'
 | 
				
			||||||
 | 
					    extClass = '.Dilar'
 | 
				
			||||||
 | 
					    themePkg = 'gmanga'
 | 
				
			||||||
 | 
					    overrideVersionCode = 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apply from: "$rootDir/common.gradle"
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/ar/dilar/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/dilar/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/dilar/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/dilar/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 6.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/dilar/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 9.0 KiB  | 
@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ar.dilar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.SChapter
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.SManga
 | 
				
			||||||
 | 
					import okhttp3.Request
 | 
				
			||||||
 | 
					import okhttp3.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Dilar : Gmanga(
 | 
				
			||||||
 | 
					    "Dilar",
 | 
				
			||||||
 | 
					    "https://dilar.tube",
 | 
				
			||||||
 | 
					    "ar",
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    override fun chaptersRequest(manga: SManga): Request {
 | 
				
			||||||
 | 
					        val mangaId = manga.url.substringAfterLast("/")
 | 
				
			||||||
 | 
					        return GET("$baseUrl/api/mangas/$mangaId/releases", headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chaptersParse(response: Response): List<SChapter> {
 | 
				
			||||||
 | 
					        val releases = response.parseAs<ChapterListDto>().releases
 | 
				
			||||||
 | 
					            .filterNot { it.isMonetized }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return releases.map { it.toSChapter() }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ar.dilar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.SChapter
 | 
				
			||||||
 | 
					import kotlinx.serialization.SerialName
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.JsonPrimitive
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.float
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ChapterListDto(
 | 
				
			||||||
 | 
					    val releases: List<ChapterRelease>,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ChapterRelease(
 | 
				
			||||||
 | 
					    private val id: Int,
 | 
				
			||||||
 | 
					    private val chapter: JsonPrimitive,
 | 
				
			||||||
 | 
					    private val title: String,
 | 
				
			||||||
 | 
					    @SerialName("team_name") private val teamName: String,
 | 
				
			||||||
 | 
					    @SerialName("time_stamp") private val timestamp: Long,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @SerialName("has_rev_link") private val hasRevLink: Boolean,
 | 
				
			||||||
 | 
					    @SerialName("support_link") private val supportLink: String,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    val isMonetized get() = hasRevLink && supportLink.isNotEmpty()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fun toSChapter() = SChapter.create().apply {
 | 
				
			||||||
 | 
					        url = "/r/$id"
 | 
				
			||||||
 | 
					        chapter_number = chapter.float
 | 
				
			||||||
 | 
					        date_upload = timestamp * 1000
 | 
				
			||||||
 | 
					        scanlator = teamName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val chapterName = title.let { if (it.trim() != "") " - $it" else "" }
 | 
				
			||||||
 | 
					        name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,7 +1,8 @@
 | 
				
			|||||||
ext {
 | 
					ext {
 | 
				
			||||||
    extName = 'GMANGA'
 | 
					    extName = 'GMANGA'
 | 
				
			||||||
    extClass = '.Gmanga'
 | 
					    extClass = '.Gmanga'
 | 
				
			||||||
    extVersionCode = 13
 | 
					    themePkg = 'gmanga'
 | 
				
			||||||
 | 
					    overrideVersionCode = 13
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
apply from: "$rootDir/common.gradle"
 | 
					apply from: "$rootDir/common.gradle"
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ar.gmanga
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import kotlinx.serialization.SerialName
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.JsonPrimitive
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ChapterListResponse(
 | 
				
			||||||
 | 
					    val releases: List<ChapterRelease>,
 | 
				
			||||||
 | 
					    val chapterizations: List<Chapterization>,
 | 
				
			||||||
 | 
					    val teams: List<Team>,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ChapterRelease(
 | 
				
			||||||
 | 
					    val id: Int,
 | 
				
			||||||
 | 
					    @SerialName("chapterization_id") val chapId: Int,
 | 
				
			||||||
 | 
					    @SerialName("team_id") val teamId: Int,
 | 
				
			||||||
 | 
					    val chapter: JsonPrimitive,
 | 
				
			||||||
 | 
					    @SerialName("time_stamp") val timestamp: Long,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class Chapterization(
 | 
				
			||||||
 | 
					    val id: Int,
 | 
				
			||||||
 | 
					    val title: String,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class Team(
 | 
				
			||||||
 | 
					    val id: Int,
 | 
				
			||||||
 | 
					    val name: String,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@ -1,315 +1,90 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
 | 
					package eu.kanade.tachiyomi.extension.ar.gmanga
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import androidx.preference.PreferenceScreen
 | 
					import android.app.Application
 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING
 | 
					import eu.kanade.tachiyomi.multisrc.gmanga.BrowseManga
 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING_SHOW_ALL
 | 
					import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING_SHOW_POPULAR
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_LASTETS_LISTING
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.dto.TableDto
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.extension.ar.gmanga.dto.asChapterList
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.network.GET
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
import eu.kanade.tachiyomi.network.POST
 | 
					import eu.kanade.tachiyomi.network.asObservable
 | 
				
			||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
					import eu.kanade.tachiyomi.network.interceptor.rateLimit
 | 
				
			||||||
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.MangasPage
 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.Page
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.SChapter
 | 
					import eu.kanade.tachiyomi.source.model.SChapter
 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.SManga
 | 
					import eu.kanade.tachiyomi.source.model.SManga
 | 
				
			||||||
import eu.kanade.tachiyomi.source.online.HttpSource
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.util.asJsoup
 | 
					 | 
				
			||||||
import kotlinx.serialization.decodeFromString
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.Json
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.JsonObject
 | 
					import kotlinx.serialization.json.JsonObject
 | 
				
			||||||
import kotlinx.serialization.json.buildJsonArray
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.contentOrNull
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.decodeFromJsonElement
 | 
					import kotlinx.serialization.json.decodeFromJsonElement
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.float
 | 
				
			||||||
import kotlinx.serialization.json.jsonArray
 | 
					import kotlinx.serialization.json.jsonArray
 | 
				
			||||||
import kotlinx.serialization.json.jsonObject
 | 
					import kotlinx.serialization.json.jsonObject
 | 
				
			||||||
import kotlinx.serialization.json.jsonPrimitive
 | 
					 | 
				
			||||||
import okhttp3.Headers
 | 
					 | 
				
			||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
 | 
					 | 
				
			||||||
import okhttp3.OkHttpClient
 | 
					 | 
				
			||||||
import okhttp3.Request
 | 
					import okhttp3.Request
 | 
				
			||||||
import okhttp3.RequestBody.Companion.toRequestBody
 | 
					 | 
				
			||||||
import okhttp3.Response
 | 
					import okhttp3.Response
 | 
				
			||||||
import uy.kohesive.injekt.injectLazy
 | 
					import rx.Observable
 | 
				
			||||||
import java.text.SimpleDateFormat
 | 
					import uy.kohesive.injekt.Injekt
 | 
				
			||||||
import java.util.Locale
 | 
					import uy.kohesive.injekt.api.get
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Gmanga : ConfigurableSource, HttpSource() {
 | 
					class Gmanga : Gmanga(
 | 
				
			||||||
 | 
					    "GMANGA",
 | 
				
			||||||
    private val domain: String = "gmanga.org"
 | 
					    "https://gmanga.org",
 | 
				
			||||||
 | 
					    "ar",
 | 
				
			||||||
    override val baseUrl: String = "https://$domain"
 | 
					    "https://media.gmanga.me",
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
    override val lang: String = "ar"
 | 
					    override val client = super.client.newBuilder()
 | 
				
			||||||
 | 
					 | 
				
			||||||
    override val name: String = "GMANGA"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override val supportsLatest: Boolean = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private val json: Json by injectLazy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private val preferences = GmangaPreferences(id)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override val client: OkHttpClient = network.client.newBuilder()
 | 
					 | 
				
			||||||
        .rateLimit(4)
 | 
					        .rateLimit(4)
 | 
				
			||||||
        .build()
 | 
					        .build()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private val parsedDatePattern: SimpleDateFormat = SimpleDateFormat(
 | 
					    init {
 | 
				
			||||||
        "yyyy-MM-dd HH:mm:ss ZZZ zzz",
 | 
					        // remove obsolete preferences
 | 
				
			||||||
        Locale.ENGLISH,
 | 
					        Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).run {
 | 
				
			||||||
    )
 | 
					            if (contains("gmanga_chapter_listing")) {
 | 
				
			||||||
    private val formattedDatePattern: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
 | 
					                edit().remove("gmanga_chapter_listing").apply()
 | 
				
			||||||
    override fun headersBuilder() = Headers.Builder().apply {
 | 
					            }
 | 
				
			||||||
        add("User-Agent", USER_AGENT)
 | 
					            if (contains("gmanga_last_listing")) {
 | 
				
			||||||
 | 
					                edit().remove("gmanga_last_listing").apply()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override fun setupPreferenceScreen(screen: PreferenceScreen) =
 | 
					    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
				
			||||||
        preferences.setupPreferenceScreen(screen)
 | 
					        val decMga = response.decryptAs<JsonObject>()
 | 
				
			||||||
 | 
					        val selectedManga = decMga["rows"]!!.jsonArray[0].jsonObject["rows"]!!.jsonArray
 | 
				
			||||||
    override fun chapterListRequest(manga: SManga): Request {
 | 
					        val manags = selectedManga.map {
 | 
				
			||||||
        val mangaId = manga.url.substringAfterLast("/")
 | 
					            json.decodeFromJsonElement<BrowseManga>(it.jsonArray[17])
 | 
				
			||||||
        return GET("$baseUrl/api/mangas/$mangaId/releases", headers)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun chapterListParse(response: Response): List<SChapter> {
 | 
					 | 
				
			||||||
        val data = decryptResponse(response)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val table = json.decodeFromJsonElement<TableDto>(data)
 | 
					 | 
				
			||||||
        val chapterList = table.asChapterList()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val releases = when (preferences.getString(PREF_CHAPTER_LISTING)) {
 | 
					 | 
				
			||||||
            PREF_CHAPTER_LISTING_SHOW_POPULAR ->
 | 
					 | 
				
			||||||
                chapterList.releases
 | 
					 | 
				
			||||||
                    .groupBy { release -> release.chapterizationId }
 | 
					 | 
				
			||||||
                    .mapNotNull { (_, releases) -> releases.maxByOrNull { it.views } }
 | 
					 | 
				
			||||||
            PREF_CHAPTER_LISTING_SHOW_ALL -> chapterList.releases
 | 
					 | 
				
			||||||
            else -> emptyList()
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return releases.map { release ->
 | 
					        val entries = manags.map { it.toSManga(::createThumbnail) }
 | 
				
			||||||
            SChapter.create().apply {
 | 
					            .distinctBy { it.url }
 | 
				
			||||||
                val chapter = chapterList.chapters.first { it.id == release.chapterizationId }
 | 
					 | 
				
			||||||
                val team = chapterList.teams.firstOrNull { it.id == release.teamId }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                url = "/r/${release.id}"
 | 
					        return MangasPage(
 | 
				
			||||||
                chapter_number = chapter.chapter
 | 
					            entries,
 | 
				
			||||||
                date_upload = release.timestamp * 1000
 | 
					            hasNextPage = (manags.size >= 30),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
 | 
				
			||||||
 | 
					        return client.newCall(chapterListRequest(manga))
 | 
				
			||||||
 | 
					            .asObservable() // sites returns false 302 code
 | 
				
			||||||
 | 
					            .map(::chapterListParse)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chaptersRequest(manga: SManga): Request {
 | 
				
			||||||
 | 
					        val mangaId = manga.url.substringAfterLast("/")
 | 
				
			||||||
 | 
					        return GET("https://api2.gmanga.me/api/mangas/$mangaId/releases", headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chaptersParse(response: Response): List<SChapter> {
 | 
				
			||||||
 | 
					        val chapterList = response.parseAs<ChapterListResponse>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return chapterList.releases.map {
 | 
				
			||||||
 | 
					            SChapter.create().apply {
 | 
				
			||||||
 | 
					                val chapter = chapterList.chapterizations.first { chap -> chap.id == it.chapId }
 | 
				
			||||||
 | 
					                val team = chapterList.teams.firstOrNull { team -> team.id == it.teamId }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                url = "/r/${it.id}"
 | 
				
			||||||
 | 
					                chapter_number = it.chapter.float
 | 
				
			||||||
 | 
					                date_upload = it.timestamp * 1000
 | 
				
			||||||
                scanlator = team?.name
 | 
					                scanlator = team?.name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                val chapterName = chapter.title.let { if (it.trim() != "") " - $it" else "" }
 | 
					                val chapterName = chapter.title.let { if (it.trim() != "") " - $it" else "" }
 | 
				
			||||||
                name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
 | 
					                name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }.sortedWith(compareBy({ -it.chapter_number }, { -it.date_upload }))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun imageUrlParse(response: Response): String =
 | 
					 | 
				
			||||||
        throw UnsupportedOperationException()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun latestUpdatesParse(response: Response): MangasPage {
 | 
					 | 
				
			||||||
        val isLatest = when (preferences.getString(PREF_LASTETS_LISTING)) {
 | 
					 | 
				
			||||||
            PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA -> true
 | 
					 | 
				
			||||||
            PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER -> false
 | 
					 | 
				
			||||||
            else -> true
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        val mangas = if (!isLatest) {
 | 
					 | 
				
			||||||
            val decMga = decryptResponse(response)
 | 
					 | 
				
			||||||
            val selectedManga = decMga["rows"]!!.jsonArray[0].jsonObject["rows"]!!.jsonArray
 | 
					 | 
				
			||||||
            buildJsonArray {
 | 
					 | 
				
			||||||
                for (i in 0 until selectedManga.size) {
 | 
					 | 
				
			||||||
                    add(selectedManga[i].jsonArray[17])
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            val data = json.decodeFromString<JsonObject>(
 | 
					 | 
				
			||||||
                response.asJsoup().select(".js-react-on-rails-component").html(),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            data["mangaDataAction"]!!.jsonObject["newMangas"]!!.jsonArray
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return MangasPage(
 | 
					 | 
				
			||||||
            mangas.jsonArray.map {
 | 
					 | 
				
			||||||
                SManga.create().apply {
 | 
					 | 
				
			||||||
                    url = "/mangas/${it.jsonObject["id"]!!.jsonPrimitive.content}"
 | 
					 | 
				
			||||||
                    title = it.jsonObject["title"]!!.jsonPrimitive.content
 | 
					 | 
				
			||||||
                    val thumbnail = "medium_${
 | 
					 | 
				
			||||||
                    it.jsonObject["cover"]!!.jsonPrimitive.content.substringBeforeLast(".")
 | 
					 | 
				
			||||||
                    }.webp"
 | 
					 | 
				
			||||||
                    thumbnail_url =
 | 
					 | 
				
			||||||
                        "https://media.gmanga.me/uploads/manga/cover/${it.jsonObject["id"]!!.jsonPrimitive.content}/$thumbnail"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            (mangas.size >= 30) && !isLatest,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun latestUpdatesRequest(page: Int): Request {
 | 
					 | 
				
			||||||
        val latestUrl = when (preferences.getString(PREF_LASTETS_LISTING)) {
 | 
					 | 
				
			||||||
            PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA -> "$baseUrl/mangas/latest"
 | 
					 | 
				
			||||||
            PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER -> "https://api.gmanga.me/api/releases?page=$page"
 | 
					 | 
				
			||||||
            else -> "$baseUrl/mangas/latest"
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return GET(latestUrl, headers)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun mangaDetailsParse(response: Response): SManga {
 | 
					 | 
				
			||||||
        val altNamePrefix = "مسميّات أخرى"
 | 
					 | 
				
			||||||
        val translationStatusPrefix = "حالة الترجمة"
 | 
					 | 
				
			||||||
        val startedDayPrefix = "تاريخ النشر"
 | 
					 | 
				
			||||||
        val endedDayPrefix = "تاريخ الانتهاء"
 | 
					 | 
				
			||||||
        val data = json.decodeFromString<JsonObject>(
 | 
					 | 
				
			||||||
            response.asJsoup().select(".js-react-on-rails-component").html(),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        val mangaData = data["mangaDataAction"]!!.jsonObject["mangaData"]!!.jsonObject
 | 
					 | 
				
			||||||
        return SManga.create().apply {
 | 
					 | 
				
			||||||
            description =
 | 
					 | 
				
			||||||
                mangaData["summary"]!!.jsonPrimitive.contentOrNull?.ifEmpty { "لم يتم اضافة قصة بعد" }
 | 
					 | 
				
			||||||
            artist =
 | 
					 | 
				
			||||||
                mangaData["artists"]!!.jsonArray.joinToString(", ") { it.jsonObject["name"]!!.jsonPrimitive.content }
 | 
					 | 
				
			||||||
            author =
 | 
					 | 
				
			||||||
                mangaData["authors"]!!.jsonArray.joinToString(", ") { it.jsonObject["name"]!!.jsonPrimitive.content }
 | 
					 | 
				
			||||||
            status = parseStatus(mangaData["story_status"].toString())
 | 
					 | 
				
			||||||
            genre = listOfNotNull(
 | 
					 | 
				
			||||||
                mangaData["type"]!!.jsonObject["title"]!!.jsonPrimitive.content,
 | 
					 | 
				
			||||||
                mangaData["type"]!!.jsonObject["name"]!!.jsonPrimitive.content,
 | 
					 | 
				
			||||||
                mangaData["categories"]!!.jsonArray.joinToString(", ") { it.jsonObject["name"]!!.jsonPrimitive.content },
 | 
					 | 
				
			||||||
            ).joinToString(", ")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            parseTranslationStatus(mangaData["translation_status"].toString()).let {
 | 
					 | 
				
			||||||
                description = "$description\n\n:$translationStatusPrefix ᗏ \n$it •"
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            var startedDate =
 | 
					 | 
				
			||||||
                mangaData["s_date"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() }
 | 
					 | 
				
			||||||
            startedDate = if (startedDate.isNullOrBlank().not()) {
 | 
					 | 
				
			||||||
                parsedDatePattern.parse(startedDate!!)?.let { formattedDatePattern.format(it) }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                null
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            var endedDay = mangaData["e_date"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() }
 | 
					 | 
				
			||||||
            endedDay = if (endedDay.isNullOrBlank().not()) {
 | 
					 | 
				
			||||||
                parsedDatePattern.parse(endedDay!!)?.let { formattedDatePattern.format(it) }
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                null
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            val alternativeName = listOfNotNull(
 | 
					 | 
				
			||||||
                mangaData["synonyms"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
 | 
					 | 
				
			||||||
                mangaData["arabic_title"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
 | 
					 | 
				
			||||||
                mangaData["japanese"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
 | 
					 | 
				
			||||||
                mangaData["english"]!!.jsonPrimitive.content.takeIf { it.isBlank().not() },
 | 
					 | 
				
			||||||
            ).joinToString("\n").trim()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            val additionalInformation = listOfNotNull(
 | 
					 | 
				
			||||||
                startedDate,
 | 
					 | 
				
			||||||
                endedDay,
 | 
					 | 
				
			||||||
                alternativeName,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            additionalInformation.forEach { info ->
 | 
					 | 
				
			||||||
                when (info) {
 | 
					 | 
				
			||||||
                    startedDate ->
 | 
					 | 
				
			||||||
                        description =
 | 
					 | 
				
			||||||
                            "$description\n\n:$startedDayPrefix ᗏ \n$startedDate •"
 | 
					 | 
				
			||||||
                    endedDay -> description = "$description\n\n:$endedDayPrefix ᗏ \n$endedDay •"
 | 
					 | 
				
			||||||
                    alternativeName ->
 | 
					 | 
				
			||||||
                        description =
 | 
					 | 
				
			||||||
                            "$description\n\n:$altNamePrefix ᗏ \n$alternativeName •"
 | 
					 | 
				
			||||||
                    else -> description
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun parseStatus(status: String?) = when {
 | 
					 | 
				
			||||||
        status == null -> SManga.UNKNOWN
 | 
					 | 
				
			||||||
        status.contains("2") -> SManga.ONGOING
 | 
					 | 
				
			||||||
        status.contains("3") -> SManga.COMPLETED
 | 
					 | 
				
			||||||
        else -> SManga.UNKNOWN
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun parseTranslationStatus(status: String?) = when {
 | 
					 | 
				
			||||||
        status == null -> "مجهول"
 | 
					 | 
				
			||||||
        status.contains("0") -> "منتهية"
 | 
					 | 
				
			||||||
        status.contains("1") -> "مستمرة"
 | 
					 | 
				
			||||||
        status.contains("2") -> "متوقفة"
 | 
					 | 
				
			||||||
        else -> "مجهول"
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun pageListParse(response: Response): List<Page> {
 | 
					 | 
				
			||||||
        val url = response.request.url.toString()
 | 
					 | 
				
			||||||
        val data = json.decodeFromString<JsonObject>(
 | 
					 | 
				
			||||||
            response.asJsoup().select(".js-react-on-rails-component").html(),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        val releaseData =
 | 
					 | 
				
			||||||
            data["readerDataAction"]!!.jsonObject["readerData"]!!.jsonObject["release"]!!.jsonObject
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val hasWebP = releaseData["webp_pages"]!!.jsonArray.size > 0
 | 
					 | 
				
			||||||
        return releaseData[if (hasWebP) "webp_pages" else "pages"]!!.jsonArray.map { it.jsonPrimitive.content }
 | 
					 | 
				
			||||||
            .sortedWith(pageSort)
 | 
					 | 
				
			||||||
            .mapIndexed { index, pageUri ->
 | 
					 | 
				
			||||||
                Page(
 | 
					 | 
				
			||||||
                    index,
 | 
					 | 
				
			||||||
                    "$url#page_$index",
 | 
					 | 
				
			||||||
                    "https://media.gmanga.me/uploads/releases/${releaseData["storage_key"]!!.jsonPrimitive.content}/hq${if (hasWebP) "_webp" else ""}/$pageUri",
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private val pageSort =
 | 
					 | 
				
			||||||
        compareBy<String>({ parseNumber(0, it) ?: Double.MAX_VALUE }, { parseNumber(1, it) }, { parseNumber(2, it) })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun parseNumber(index: Int, string: String): Double? =
 | 
					 | 
				
			||||||
        Regex("\\d+").findAll(string).map { it.value }.toList().getOrNull(index)?.toDoubleOrNull()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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"]!!.jsonArray
 | 
					 | 
				
			||||||
        return MangasPage(
 | 
					 | 
				
			||||||
            mangas.jsonArray.map {
 | 
					 | 
				
			||||||
                SManga.create().apply {
 | 
					 | 
				
			||||||
                    url = "/mangas/${it.jsonObject["id"]!!.jsonPrimitive.content}"
 | 
					 | 
				
			||||||
                    title = it.jsonObject["title"]!!.jsonPrimitive.content
 | 
					 | 
				
			||||||
                    val thumbnail = "medium_${
 | 
					 | 
				
			||||||
                    it.jsonObject["cover"]!!.jsonPrimitive.content.substringBeforeLast(".")
 | 
					 | 
				
			||||||
                    }.webp"
 | 
					 | 
				
			||||||
                    thumbnail_url =
 | 
					 | 
				
			||||||
                        "https://media.gmanga.me/uploads/manga/cover/${it.jsonObject["id"]!!.jsonPrimitive.content}/$thumbnail"
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            mangas.size == 50,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    private fun decryptResponse(response: Response): JsonObject {
 | 
					 | 
				
			||||||
        val encryptedData =
 | 
					 | 
				
			||||||
            json.decodeFromString<JsonObject>(response.body.string())["data"]!!.jsonPrimitive.content
 | 
					 | 
				
			||||||
        val decryptedData = decrypt(encryptedData)
 | 
					 | 
				
			||||||
        return json.decodeFromString(decryptedData)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
 | 
					 | 
				
			||||||
        return GmangaFilters.buildSearchPayload(
 | 
					 | 
				
			||||||
            page,
 | 
					 | 
				
			||||||
            query,
 | 
					 | 
				
			||||||
            if (filters.isEmpty()) getFilterList() else filters,
 | 
					 | 
				
			||||||
        ).let {
 | 
					 | 
				
			||||||
            val body = it.toString().toRequestBody(MEDIA_TYPE)
 | 
					 | 
				
			||||||
            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 = "application/json; charset=utf-8".toMediaTypeOrNull()
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,291 +0,0 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import android.annotation.SuppressLint
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.Filter
 | 
					 | 
				
			||||||
import eu.kanade.tachiyomi.source.model.FilterList
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.JsonNull
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.JsonObject
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.JsonObjectBuilder
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.add
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.buildJsonObject
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.put
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.putJsonArray
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.putJsonObject
 | 
					 | 
				
			||||||
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 buildJsonObject {
 | 
					 | 
				
			||||||
                oneShotFilter.state.first().let {
 | 
					 | 
				
			||||||
                    putJsonObject("oneshot") {
 | 
					 | 
				
			||||||
                        when {
 | 
					 | 
				
			||||||
                            it.isIncluded() -> put("value", true)
 | 
					 | 
				
			||||||
                            it.isExcluded() -> put("value", false)
 | 
					 | 
				
			||||||
                            else -> put("value", JsonNull)
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                put("title", query)
 | 
					 | 
				
			||||||
                put("page", page)
 | 
					 | 
				
			||||||
                putJsonObject("manga_types") {
 | 
					 | 
				
			||||||
                    putJsonArray("include") {
 | 
					 | 
				
			||||||
                        mangaTypeFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    putJsonArray("exclude") {
 | 
					 | 
				
			||||||
                        mangaTypeFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                putJsonObject("story_status") {
 | 
					 | 
				
			||||||
                    putJsonArray("include") {
 | 
					 | 
				
			||||||
                        storyStatusFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    putJsonArray("exclude") {
 | 
					 | 
				
			||||||
                        storyStatusFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                putJsonObject("translation_status") {
 | 
					 | 
				
			||||||
                    putJsonArray("include") {
 | 
					 | 
				
			||||||
                        translationStatusFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    putJsonArray("exclude") {
 | 
					 | 
				
			||||||
                        translationStatusFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                putJsonObject("categories") {
 | 
					 | 
				
			||||||
                    putJsonArray("include") {
 | 
					 | 
				
			||||||
                        add(JsonNull) // always included, maybe to avoid shifting index in the backend
 | 
					 | 
				
			||||||
                        categoryFilter.state.filter { it.isIncluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    putJsonArray("exclude") {
 | 
					 | 
				
			||||||
                        categoryFilter.state.filter { it.isExcluded() }.map { it.id }.forEach { add(it) }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                putJsonObject("chapters") {
 | 
					 | 
				
			||||||
                    putFromValidatingTextFilter(
 | 
					 | 
				
			||||||
                        chapterCountFilter.state.first {
 | 
					 | 
				
			||||||
                            it.id == FILTER_ID_MIN_CHAPTER_COUNT
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        "min",
 | 
					 | 
				
			||||||
                        ERROR_INVALID_MIN_CHAPTER_COUNT,
 | 
					 | 
				
			||||||
                        "",
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    putFromValidatingTextFilter(
 | 
					 | 
				
			||||||
                        chapterCountFilter.state.first {
 | 
					 | 
				
			||||||
                            it.id == FILTER_ID_MAX_CHAPTER_COUNT
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        "max",
 | 
					 | 
				
			||||||
                        ERROR_INVALID_MAX_CHAPTER_COUNT,
 | 
					 | 
				
			||||||
                        "",
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                putJsonObject("dates") {
 | 
					 | 
				
			||||||
                    putFromValidatingTextFilter(
 | 
					 | 
				
			||||||
                        dateRangeFilter.state.first {
 | 
					 | 
				
			||||||
                            it.id == FILTER_ID_START_DATE
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        "start",
 | 
					 | 
				
			||||||
                        ERROR_INVALID_START_DATE,
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    putFromValidatingTextFilter(
 | 
					 | 
				
			||||||
                        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 JsonObjectBuilder.putFromValidatingTextFilter(
 | 
					 | 
				
			||||||
            filter: ValidatingTextFilter,
 | 
					 | 
				
			||||||
            property: String,
 | 
					 | 
				
			||||||
            invalidErrorMessage: String,
 | 
					 | 
				
			||||||
            default: String? = null,
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
            filter.let {
 | 
					 | 
				
			||||||
                when {
 | 
					 | 
				
			||||||
                    it.state == "" -> if (default == null) {
 | 
					 | 
				
			||||||
                        put(property, JsonNull)
 | 
					 | 
				
			||||||
                    } else {
 | 
					 | 
				
			||||||
                        put(property, default)
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    it.isValid() -> put(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
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,81 +0,0 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import android.app.Application
 | 
					 | 
				
			||||||
import android.content.SharedPreferences
 | 
					 | 
				
			||||||
import androidx.preference.ListPreference
 | 
					 | 
				
			||||||
import androidx.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 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"
 | 
					 | 
				
			||||||
        const val PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER = "gmanga_Last_listing_last_chapter_added"
 | 
					 | 
				
			||||||
        const val PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA = "gmanga_chapter_listing_last_manga_added"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        val PREF_CHAPTER_LISTING = StringPreference(
 | 
					 | 
				
			||||||
            "gmanga_chapter_listing",
 | 
					 | 
				
			||||||
            "كيفية عرض الفصل بقائمة الفصول",
 | 
					 | 
				
			||||||
            listOf(
 | 
					 | 
				
			||||||
                StringPreferenceOption(PREF_CHAPTER_LISTING_SHOW_POPULAR, "اختيار النسخة الأكثر مشاهدة"),
 | 
					 | 
				
			||||||
                StringPreferenceOption(PREF_CHAPTER_LISTING_SHOW_ALL, "عرض جميع النسخ"),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        val PREF_LASTETS_LISTING = StringPreference(
 | 
					 | 
				
			||||||
            "gmanga_last_listing",
 | 
					 | 
				
			||||||
            "كيفية عرض بقائمة الأعمال الجديدة ",
 | 
					 | 
				
			||||||
            listOf(
 | 
					 | 
				
			||||||
                StringPreferenceOption(PREF_LASTETS_LISTING_SHOW_LASTETS_CHAPTER, "اختيار آخر الإضافات"),
 | 
					 | 
				
			||||||
                StringPreferenceOption(PREF_LASTETS_LISTING_SHOW_LASTETS_MANGA, "اختيار لمانجات الجديدة"),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private val STRING_PREFERENCES = listOf(
 | 
					 | 
				
			||||||
            PREF_CHAPTER_LISTING,
 | 
					 | 
				
			||||||
            PREF_LASTETS_LISTING,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,13 +0,0 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.serialization.SerialName
 | 
					 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Serializable
 | 
					 | 
				
			||||||
data class ChapterDto(
 | 
					 | 
				
			||||||
    val id: Int,
 | 
					 | 
				
			||||||
    val chapter: Float,
 | 
					 | 
				
			||||||
    val volume: Int,
 | 
					 | 
				
			||||||
    val title: String,
 | 
					 | 
				
			||||||
    @SerialName("time_stamp") val timestamp: Long,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -1,10 +0,0 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Serializable
 | 
					 | 
				
			||||||
data class ChapterListDto(
 | 
					 | 
				
			||||||
    val releases: List<ReleaseDto>,
 | 
					 | 
				
			||||||
    val teams: List<TeamDto>,
 | 
					 | 
				
			||||||
    val chapters: List<ChapterDto>,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -1,15 +0,0 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.serialization.SerialName
 | 
					 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Serializable
 | 
					 | 
				
			||||||
data class ReleaseDto(
 | 
					 | 
				
			||||||
    val id: Int,
 | 
					 | 
				
			||||||
    @SerialName("created_at") val createdAt: String,
 | 
					 | 
				
			||||||
    @SerialName("timestamp") val timestamp: Long,
 | 
					 | 
				
			||||||
    val views: Int,
 | 
					 | 
				
			||||||
    @SerialName("chapterization_id") val chapterizationId: Int,
 | 
					 | 
				
			||||||
    @SerialName("team_id") val teamId: Int,
 | 
					 | 
				
			||||||
    val teams: List<Int>,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -1,61 +0,0 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.Json
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.JsonElement
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.decodeFromJsonElement
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.float
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.int
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.jsonArray
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.jsonPrimitive
 | 
					 | 
				
			||||||
import kotlinx.serialization.json.long
 | 
					 | 
				
			||||||
import uy.kohesive.injekt.injectLazy
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Serializable
 | 
					 | 
				
			||||||
data class TableDto(
 | 
					 | 
				
			||||||
    val cols: List<String>,
 | 
					 | 
				
			||||||
    val rows: List<JsonElement>,
 | 
					 | 
				
			||||||
    val isCompact: Boolean,
 | 
					 | 
				
			||||||
    val maxLevel: Int,
 | 
					 | 
				
			||||||
    val isArray: Boolean? = null,
 | 
					 | 
				
			||||||
    val isObject: Boolean? = null,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
private val json: Json by injectLazy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
private fun TableDto.get(key: String): TableDto? {
 | 
					 | 
				
			||||||
    isObject ?: return null
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    val index = cols.indexOf(key)
 | 
					 | 
				
			||||||
    return json.decodeFromJsonElement(rows[index])
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
fun TableDto.asChapterList() = ChapterListDto(
 | 
					 | 
				
			||||||
    // YOLO
 | 
					 | 
				
			||||||
    get("releases")!!.rows.map {
 | 
					 | 
				
			||||||
        ReleaseDto(
 | 
					 | 
				
			||||||
            it.jsonArray[0].jsonPrimitive.int,
 | 
					 | 
				
			||||||
            it.jsonArray[1].jsonPrimitive.content,
 | 
					 | 
				
			||||||
            it.jsonArray[2].jsonPrimitive.long,
 | 
					 | 
				
			||||||
            it.jsonArray[3].jsonPrimitive.int,
 | 
					 | 
				
			||||||
            it.jsonArray[4].jsonPrimitive.int,
 | 
					 | 
				
			||||||
            it.jsonArray[5].jsonPrimitive.int,
 | 
					 | 
				
			||||||
            it.jsonArray[6].jsonArray.map { it.jsonPrimitive.int },
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    get("teams")!!.rows.map {
 | 
					 | 
				
			||||||
        TeamDto(
 | 
					 | 
				
			||||||
            it.jsonArray[0].jsonPrimitive.int,
 | 
					 | 
				
			||||||
            it.jsonArray[1].jsonPrimitive.content,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    get("chapterizations")!!.rows.map {
 | 
					 | 
				
			||||||
        ChapterDto(
 | 
					 | 
				
			||||||
            it.jsonArray[0].jsonPrimitive.int,
 | 
					 | 
				
			||||||
            it.jsonArray[1].jsonPrimitive.float,
 | 
					 | 
				
			||||||
            it.jsonArray[2].jsonPrimitive.int,
 | 
					 | 
				
			||||||
            it.jsonArray[3].jsonPrimitive.content,
 | 
					 | 
				
			||||||
            it.jsonArray[4].jsonPrimitive.long,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@ -1,9 +0,0 @@
 | 
				
			|||||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import kotlinx.serialization.Serializable
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Serializable
 | 
					 | 
				
			||||||
data class TeamDto(
 | 
					 | 
				
			||||||
    val id: Int,
 | 
					 | 
				
			||||||
    val name: String,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
							
								
								
									
										8
									
								
								src/ar/mangatales/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					ext {
 | 
				
			||||||
 | 
					    extName = 'Manga Tales'
 | 
				
			||||||
 | 
					    extClass = '.MangaTales'
 | 
				
			||||||
 | 
					    themePkg = 'gmanga'
 | 
				
			||||||
 | 
					    overrideVersionCode = 0
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					apply from: "$rootDir/common.gradle"
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								src/ar/mangatales/res/mipmap-hdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/mangatales/res/mipmap-mdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.0 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/mangatales/res/mipmap-xhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/mangatales/res/mipmap-xxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 8.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/ar/mangatales/res/mipmap-xxxhdpi/ic_launcher.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 12 KiB  | 
@ -0,0 +1,67 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ar.mangatales
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.SChapter
 | 
				
			||||||
 | 
					import kotlinx.serialization.SerialName
 | 
				
			||||||
 | 
					import kotlinx.serialization.Serializable
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.JsonPrimitive
 | 
				
			||||||
 | 
					import kotlinx.serialization.json.float
 | 
				
			||||||
 | 
					import java.text.SimpleDateFormat
 | 
				
			||||||
 | 
					import java.util.Locale
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ChapterListDto(
 | 
				
			||||||
 | 
					    val mangaReleases: List<ChapterRelease>,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ChapterRelease(
 | 
				
			||||||
 | 
					    private val id: Int,
 | 
				
			||||||
 | 
					    private val chapter: JsonPrimitive,
 | 
				
			||||||
 | 
					    private val title: String,
 | 
				
			||||||
 | 
					    @SerialName("team_name") private val teamName: String,
 | 
				
			||||||
 | 
					    @SerialName("created_at") private val createdAt: String,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    fun toSChapter() = SChapter.create().apply {
 | 
				
			||||||
 | 
					        url = "/r/$id"
 | 
				
			||||||
 | 
					        chapter_number = chapter.float
 | 
				
			||||||
 | 
					        date_upload = try {
 | 
				
			||||||
 | 
					            dateFormat.parse(createdAt)!!.time
 | 
				
			||||||
 | 
					        } catch (_: Exception) {
 | 
				
			||||||
 | 
					            0L
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        scanlator = teamName
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val chapterName = title.let { if (it.trim() != "") " - $it" else "" }
 | 
				
			||||||
 | 
					        name = "${chapter_number.let { if (it % 1 > 0) it else it.toInt() }}$chapterName"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderDto(
 | 
				
			||||||
 | 
					    val readerDataAction: ReaderData,
 | 
				
			||||||
 | 
					    val globals: Globals,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class Globals(
 | 
				
			||||||
 | 
					    val mediaKey: String,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderData(
 | 
				
			||||||
 | 
					    val readerData: ReaderChapter,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderChapter(
 | 
				
			||||||
 | 
					    val release: ReaderPages,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Serializable
 | 
				
			||||||
 | 
					class ReaderPages(
 | 
				
			||||||
 | 
					    @SerialName("hq_pages") private val page: String,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    val pages get() = page.split("\r\n")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					package eu.kanade.tachiyomi.extension.ar.mangatales
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.multisrc.gmanga.Gmanga
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.multisrc.gmanga.TagFilterData
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.network.GET
 | 
				
			||||||
 | 
					import eu.kanade.tachiyomi.source.model.Filter
 | 
				
			||||||
 | 
					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.util.asJsoup
 | 
				
			||||||
 | 
					import okhttp3.Request
 | 
				
			||||||
 | 
					import okhttp3.Response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MangaTales : Gmanga(
 | 
				
			||||||
 | 
					    "Manga Tales",
 | 
				
			||||||
 | 
					    "https://www.mangatales.com",
 | 
				
			||||||
 | 
					    "ar",
 | 
				
			||||||
 | 
					    "https://media.mangatales.com",
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					    override fun createThumbnail(mangaId: String, cover: String): String {
 | 
				
			||||||
 | 
					        return "$cdnUrl/uploads/manga/cover/$mangaId/large_$cover"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun getTypesFilter() = listOf(
 | 
				
			||||||
 | 
					        TagFilterData("1", "عربية", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					        TagFilterData("2", "إنجليزي", Filter.TriState.STATE_INCLUDE),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chaptersRequest(manga: SManga): Request {
 | 
				
			||||||
 | 
					        val mangaId = manga.url.substringAfterLast("/")
 | 
				
			||||||
 | 
					        return GET("$baseUrl/api/mangas/$mangaId", headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun chaptersParse(response: Response): List<SChapter> {
 | 
				
			||||||
 | 
					        val releases = response.parseAs<ChapterListDto>().mangaReleases
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return releases.map { it.toSChapter() }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    override fun pageListParse(response: Response): List<Page> {
 | 
				
			||||||
 | 
					        val data = response.asJsoup()
 | 
				
			||||||
 | 
					            .select(".js-react-on-rails-component").html()
 | 
				
			||||||
 | 
					            .parseAs<ReaderDto>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return data.readerDataAction.readerData.release.pages
 | 
				
			||||||
 | 
					            .mapIndexed { idx, img ->
 | 
				
			||||||
 | 
					                Page(idx, imageUrl = "$cdnUrl/uploads/releases/$img?ak=${data.globals.mediaKey}")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||