Initial commit
2
src/ar/gmanga/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/ar/gmanga/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'GMANGA'
|
||||
pkgNameSuffix = 'ar.gmanga'
|
||||
extClass = '.Gmanga'
|
||||
extVersionCode = 13
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ar/gmanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/ar/gmanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/ar/gmanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/ar/gmanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/ar/gmanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/ar/gmanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
@@ -0,0 +1,315 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING
|
||||
import eu.kanade.tachiyomi.extension.ar.gmanga.GmangaPreferences.Companion.PREF_CHAPTER_LISTING_SHOW_ALL
|
||||
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.POST
|
||||
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.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.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
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.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Gmanga : ConfigurableSource, HttpSource() {
|
||||
|
||||
private val domain: String = "gmanga.org"
|
||||
|
||||
override val baseUrl: String = "https://$domain"
|
||||
|
||||
override val lang: String = "ar"
|
||||
|
||||
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)
|
||||
.build()
|
||||
|
||||
private val parsedDatePattern: SimpleDateFormat = SimpleDateFormat(
|
||||
"yyyy-MM-dd HH:mm:ss ZZZ zzz",
|
||||
Locale.ENGLISH,
|
||||
)
|
||||
private val formattedDatePattern: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH)
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", USER_AGENT)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) =
|
||||
preferences.setupPreferenceScreen(screen)
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val mangaId = manga.url.substringAfterLast("/")
|
||||
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 ->
|
||||
SChapter.create().apply {
|
||||
val chapter = chapterList.chapters.first { it.id == release.chapterizationId }
|
||||
val team = chapterList.teams.firstOrNull { it.id == release.teamId }
|
||||
|
||||
url = "/r/${release.id}"
|
||||
chapter_number = chapter.chapter
|
||||
date_upload = release.timestamp * 1000
|
||||
scanlator = team?.name
|
||||
|
||||
val chapterName = chapter.title.let { if (it.trim() != "") " - $it" else "" }
|
||||
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("Not used")
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.gmanga
|
||||
|
||||
import android.util.Base64
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
fun decrypt(responseData: String): String {
|
||||
val enc = responseData.split("|")
|
||||
val secretKey = enc[3].sha256().hexStringToByteArray()
|
||||
|
||||
return enc[0].aesDecrypt(secretKey, enc[2])
|
||||
}
|
||||
|
||||
private fun String.hexStringToByteArray(): ByteArray {
|
||||
val len = this.length
|
||||
val data = ByteArray(len / 2)
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
data[i / 2] = (
|
||||
(Character.digit(this[i], 16) shl 4) +
|
||||
Character.digit(this[i + 1], 16)
|
||||
).toByte()
|
||||
i += 2
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private fun String.sha256(): String {
|
||||
return MessageDigest
|
||||
.getInstance("SHA-256")
|
||||
.digest(this.toByteArray())
|
||||
.fold("", { str, it -> str + "%02x".format(it) })
|
||||
}
|
||||
|
||||
private fun String.aesDecrypt(secretKey: ByteArray, ivString: String): String {
|
||||
val c = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val sk = SecretKeySpec(secretKey, "AES")
|
||||
val iv = IvParameterSpec(Base64.decode(ivString.toByteArray(Charsets.UTF_8), Base64.DEFAULT))
|
||||
c.init(Cipher.DECRYPT_MODE, sk, iv)
|
||||
|
||||
val byteStr = Base64.decode(this.toByteArray(Charsets.UTF_8), Base64.DEFAULT)
|
||||
return String(c.doFinal(byteStr))
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
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>,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
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>,
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.gmanga.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class TeamDto(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
2
src/ar/mangaae/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/ar/mangaae/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Manga.ae'
|
||||
pkgNameSuffix = 'ar.mangaae'
|
||||
extClass = '.MangaAe'
|
||||
extVersionCode = 11
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ar/mangaae/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/ar/mangaae/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
src/ar/mangaae/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src/ar/mangaae/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/ar/mangaae/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src/ar/mangaae/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -0,0 +1,185 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.mangaae
|
||||
|
||||
import android.app.Application
|
||||
import android.widget.Toast
|
||||
import androidx.preference.EditTextPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
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.ParsedHttpSource
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaAe : ParsedHttpSource(), ConfigurableSource {
|
||||
|
||||
override val name = "مانجا العرب"
|
||||
|
||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||
|
||||
private val preferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Referer", "$baseUrl/")
|
||||
.set("Origin", baseUrl)
|
||||
|
||||
// ============================== Popular ===============================
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/manga/page:$page", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.mangacontainer"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")?.run {
|
||||
attr("data-pagespeed-lazy-src").ifEmpty { attr("src") }
|
||||
}
|
||||
element.selectFirst("div.mangacontainer a.manga")!!.run {
|
||||
title = text()
|
||||
setUrlWithoutDomain(absUrl("href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.pagination a:last-child:not(.active)"
|
||||
|
||||
// =============================== Latest ===============================
|
||||
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
|
||||
|
||||
override fun latestUpdatesSelector() = "div.popular-manga-container"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
|
||||
thumbnail_url = element.selectFirst("img")?.run {
|
||||
attr("data-pagespeed-lazy-src").ifEmpty { attr("src") }
|
||||
}
|
||||
setUrlWithoutDomain(element.selectFirst("a:has(img)")!!.attr("href"))
|
||||
title = element.selectFirst("a:last-child")!!.text()
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
|
||||
// =============================== Search ===============================
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = buildString {
|
||||
append("$baseUrl/manga/search:$query|page:$page")
|
||||
filters.firstOrNull { it is OrderByFilter }
|
||||
?.takeUnless { it.state == 0 }
|
||||
?.also {
|
||||
val filter = it as OrderByFilter
|
||||
append("|order:${filter.toUriPart()}")
|
||||
}
|
||||
append("|arrange:minus")
|
||||
}
|
||||
return GET(url.toHttpUrl(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val infoElement = document.selectFirst("div.indexcontainer")!!
|
||||
// Essential info, a NPE may be understandable
|
||||
with(infoElement) {
|
||||
title = selectFirst("h1.EnglishName")!!.text().removeSurrounding("(", ")")
|
||||
author = selectFirst("div.manga-details-author h4")?.text()
|
||||
artist = author
|
||||
thumbnail_url = selectFirst("img.manga-cover")?.attr("src")
|
||||
}
|
||||
|
||||
// Additional info
|
||||
infoElement.selectFirst("div.manga-details-extended")?.run {
|
||||
status = parseStatus(selectFirst("td h4")?.text().orEmpty())
|
||||
genre = select("a[href*=tag]").eachText().joinToString()
|
||||
description = selectFirst("h4[style*=overflow-y]")?.text()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("مستمرة") -> SManga.ONGOING
|
||||
status.contains("مكتملة") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
override fun chapterListSelector() = "ul.new-manga-chapters > li a"
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href").removeSuffix("/1/") + "/0/allpages")
|
||||
name = "\u061C" + element.text() // Add unicode ARABIC LETTER MARK to ensure all titles are right to left
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div#showchaptercontainer img").mapIndexed { index, item ->
|
||||
Page(index, "", item.attr("src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw Exception("Not used")
|
||||
|
||||
// ============================== Filters ===============================
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private class OrderByFilter : UriPartFilter(
|
||||
"الترتيب حسب",
|
||||
arrayOf(
|
||||
Pair("اختيار", ""),
|
||||
Pair("اسم المانجا", "english_name"),
|
||||
Pair("تاريخ النشر", "release_date"),
|
||||
Pair("عدد الفصول", "chapter_count"),
|
||||
Pair("الحالة", "status"),
|
||||
),
|
||||
)
|
||||
|
||||
override fun getFilterList() = FilterList(OrderByFilter())
|
||||
|
||||
// ============================== Settings ==============================
|
||||
companion object {
|
||||
private const val RESTART_TACHIYOMI = ".لتطبيق الإعدادات الجديدة Tachiyomi أعد تشغيل"
|
||||
private const val BASE_URL_PREF_TITLE = "تعديل الرابط"
|
||||
private const val BASE_URL_PREF_DEFAULT = "https://manga.ae"
|
||||
private const val BASE_URL_PREF = "overrideBaseUrl_v${BuildConfig.VERSION_CODE}"
|
||||
private const val BASE_URL_PREF_SUMMARY = ".للاستخدام المؤقت. تحديث التطبيق سيؤدي الى حذف الإعدادات"
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
EditTextPreference(screen.context).apply {
|
||||
key = BASE_URL_PREF
|
||||
title = BASE_URL_PREF_TITLE
|
||||
summary = BASE_URL_PREF_SUMMARY
|
||||
setDefaultValue(BASE_URL_PREF_DEFAULT)
|
||||
dialogTitle = BASE_URL_PREF_TITLE
|
||||
|
||||
setOnPreferenceChangeListener { _, _ ->
|
||||
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, BASE_URL_PREF_DEFAULT)!!
|
||||
}
|
||||
2
src/ar/remanga/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/ar/remanga/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'RE Manga'
|
||||
pkgNameSuffix = 'ar.remanga'
|
||||
extClass = '.REManga'
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ar/remanga/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
src/ar/remanga/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/ar/remanga/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
src/ar/remanga/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/ar/remanga/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
src/ar/remanga/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,258 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.remanga
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
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.ParsedHttpSource
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class REManga : ParsedHttpSource() {
|
||||
|
||||
override val name = "RE Manga"
|
||||
|
||||
override val baseUrl = "https://re-manga.com"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/manga-list/?title=&order=popular&status=&type=")
|
||||
|
||||
override fun popularMangaSelector() = "article.animpost"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga =
|
||||
SManga.create().apply {
|
||||
setUrlWithoutDomain(element.select("a").attr("abs:href"))
|
||||
element.select("img").let {
|
||||
thumbnail_url = it.attr("abs:src")
|
||||
title = it.attr("title")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/manga-list/?title=&order=update&status=&type=")
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = "$baseUrl/manga-list/?".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("title", query)
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> url.addQueryParameter("order", filter.toUriPart())
|
||||
|
||||
is StatusFilter -> url.addQueryParameter("status", filter.toUriPart())
|
||||
|
||||
is TypeFilter -> url.addQueryParameter("type", filter.toUriPart())
|
||||
|
||||
is GenreFilter -> {
|
||||
filter.state
|
||||
.filter { it.state != Filter.TriState.STATE_IGNORE }
|
||||
.forEach { url.addQueryParameter("genre[]", it.id) }
|
||||
}
|
||||
|
||||
is YearFilter -> {
|
||||
filter.state
|
||||
.filter { it.state != Filter.TriState.STATE_IGNORE }
|
||||
.forEach { url.addQueryParameter("years[]", it.id) }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
document.select("div.infox").first()!!.let { info ->
|
||||
title = info.select("h1").text()
|
||||
}
|
||||
description = document.select("div.desc > div > p").text()
|
||||
genre = document.select("div.spe > span:contains(نوع), div.genre-info > a").joinToString { it.text() }
|
||||
document.select("div.spe > span:contains(الحالة)").first()?.text()?.also { statusText ->
|
||||
when {
|
||||
statusText.contains("مستمر", true) -> status = SManga.ONGOING
|
||||
else -> status = SManga.COMPLETED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector() = ".lsteps li"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.select("a").first()!!.attr("abs:href"))
|
||||
|
||||
val chNum = element.select(".eps > a").first()!!.text()
|
||||
val chTitle = element.select(".lchx > a").first()!!.text()
|
||||
|
||||
name = when {
|
||||
chTitle.startsWith("الفصل ") -> chTitle
|
||||
else -> "الفصل $chNum - $chTitle"
|
||||
}
|
||||
|
||||
element.select(".date").first()?.text()?.let { date ->
|
||||
date_upload = DATE_FORMATTER.parse(date)?.time ?: 0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div.reader-area img").mapIndexed { i, img ->
|
||||
Page(i, "", img.attr("abs:src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SortFilter(getSortFilters()),
|
||||
StatusFilter(getStatusFilters()),
|
||||
TypeFilter(getTypeFilter()),
|
||||
Filter.Separator(),
|
||||
Filter.Header("exclusion not available for This source"),
|
||||
GenreFilter(getGenreFilters()),
|
||||
YearFilter(getYearFilters()),
|
||||
)
|
||||
|
||||
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Sort by", vals)
|
||||
|
||||
private class TypeFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Type", vals)
|
||||
|
||||
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
||||
|
||||
class Genre(name: String, val id: String = name) : Filter.TriState(name)
|
||||
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||
|
||||
class Year(name: String, val id: String = name) : Filter.TriState(name)
|
||||
private class YearFilter(years: List<Year>) : Filter.Group<Year>("Year", years)
|
||||
|
||||
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("title", "A-Z"),
|
||||
Pair("titlereverse", "Z-A"),
|
||||
Pair("update", "Latest Update"),
|
||||
Pair("latest", "Latest Added"),
|
||||
Pair("popular", "Popular"),
|
||||
)
|
||||
|
||||
private fun getStatusFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("", "All"),
|
||||
Pair("Publishing", "مستمر"),
|
||||
Pair("Finished", "تاريخ انتهي"),
|
||||
)
|
||||
|
||||
private fun getTypeFilter(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("", "All"),
|
||||
Pair("Manga", "Manga"),
|
||||
Pair("Manhwa", "Manhwa"),
|
||||
Pair("Manhua", "Manhua"),
|
||||
)
|
||||
|
||||
private fun getGenreFilters(): List<Genre> = listOf(
|
||||
Genre("Action", "action"),
|
||||
Genre("Adventure", "adventure"),
|
||||
Genre("Comedy", "comedy"),
|
||||
Genre("Dementia", "dementia"),
|
||||
Genre("Demons", "demons"),
|
||||
Genre("Drama", "drama"),
|
||||
Genre("Ecchi", "ecchi"),
|
||||
Genre("Fantasy", "fantasy"),
|
||||
Genre("Harem", "harem"),
|
||||
Genre("Historical", "historical"),
|
||||
Genre("Horror", "horror"),
|
||||
Genre("Josei", "josei"),
|
||||
Genre("Magic", "magic"),
|
||||
Genre("Martial Arts", "martial-arts"),
|
||||
Genre("Military", "military"),
|
||||
Genre("Mystery", "mystery"),
|
||||
Genre("Parody", "parody"),
|
||||
Genre("Psychological", "psychological"),
|
||||
Genre("Romance", "romance"),
|
||||
Genre("Samurai", "samurai"),
|
||||
Genre("School", "school"),
|
||||
Genre("Sci-Fi", "sci-fi"),
|
||||
Genre("Seinen", "seinen"),
|
||||
Genre("Shounen", "shounen"),
|
||||
Genre("Slice of Life", "slice-of-life"),
|
||||
Genre("Sports", "sports"),
|
||||
Genre("Super Power", "super-power"),
|
||||
Genre("Supernatural", "supernatural"),
|
||||
Genre("Vampire", "vampire"),
|
||||
)
|
||||
|
||||
private fun getYearFilters(): List<Year> = listOf(
|
||||
Year("1970", "1970"),
|
||||
Year("1986", "1986"),
|
||||
Year("1989", "1989"),
|
||||
Year("1995", "1995"),
|
||||
Year("1997", "1997"),
|
||||
Year("1998", "1998"),
|
||||
Year("1999", "1999"),
|
||||
Year("2000", "2000"),
|
||||
Year("2002", "2002"),
|
||||
Year("2003", "2003"),
|
||||
Year("2004", "2004"),
|
||||
Year("2005", "2005"),
|
||||
Year("2006", "2006"),
|
||||
Year("2007", "2007"),
|
||||
Year("2008", "2008"),
|
||||
Year("2009", "2009"),
|
||||
Year("2010", "2010"),
|
||||
Year("2011", "2011"),
|
||||
Year("2012", "2012"),
|
||||
Year("2013", "2013"),
|
||||
Year("2014", "2014"),
|
||||
Year("2016", "2016"),
|
||||
Year("2017", "2017"),
|
||||
Year("2018", "2018"),
|
||||
Year("2019", "2019"),
|
||||
Year("2020", "2020"),
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("MMM d, yyy", Locale("ar"))
|
||||
}
|
||||
}
|
||||
}
|
||||
2
src/ar/shqqaa/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
11
src/ar/shqqaa/build.gradle
Normal file
@@ -0,0 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Shqqaa Manga'
|
||||
pkgNameSuffix = 'ar.shqqaa'
|
||||
extClass = '.Shqqaa'
|
||||
extVersionCode = 2
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ar/shqqaa/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
src/ar/shqqaa/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/ar/shqqaa/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src/ar/shqqaa/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/ar/shqqaa/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/ar/shqqaa/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
@@ -0,0 +1,124 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.shqqaa
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.ParsedHttpSource
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
class Shqqaa : ParsedHttpSource() {
|
||||
|
||||
override val name = "مانجا شقاع"
|
||||
|
||||
override val baseUrl = "https://www.shqqaa.com"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
// Popular
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.card"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("img").first()!!.attr("data-src")
|
||||
element.select("a").first()!!.let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title").split(", ")[0]
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga/chapters/", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String = "div.row > div.col-xl-3"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a").first()!!.let {
|
||||
manga.setUrlWithoutDomain("${it.attr("href").substringBeforeLast('/')}/")
|
||||
manga.title = element.select("small").first()!!.text().split(", ")[0]
|
||||
}
|
||||
manga.thumbnail_url = element.select("img").first()!!.attr("data-src")
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
// Search
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = fetchPopularManga(1)
|
||||
.map { mp -> MangasPage(mp.mangas.filter { it.title.contains(query, ignoreCase = true) }, false) }
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw Exception("Not used")
|
||||
|
||||
override fun searchMangaSelector(): String = throw Exception("Not Used")
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = throw Exception("Not Used")
|
||||
|
||||
override fun searchMangaNextPageSelector() = throw Exception("Not Used")
|
||||
|
||||
// Manga summary page
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.col-sm-12")
|
||||
val mangaInfo = infoElement[1]
|
||||
val manga = SManga.create()
|
||||
manga.title = mangaInfo.select("small.text-muted")[1].ownText().split(", ")[0]
|
||||
manga.author = null
|
||||
val status = mangaInfo.select("span.badge").first()!!.ownText()
|
||||
manga.status = parseStatus(status)
|
||||
manga.genre = null
|
||||
manga.description = infoElement.first()!!.select(".text-muted").first()!!.ownText()
|
||||
manga.thumbnail_url = mangaInfo.select("img").attr("data-src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
status.contains("مستمر") -> SManga.ONGOING
|
||||
status.contains("منتهي") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
override fun chapterListSelector() = "a.m-1"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
element.select("a").let {
|
||||
chapter.setUrlWithoutDomain(it.attr("href"))
|
||||
chapter.name = it.text()
|
||||
}
|
||||
chapter.date_upload = 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
// Pages
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
document.select("div.img-manga img").forEach {
|
||||
pages.add(Page(pages.size, "", it.attr("src")))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
||||
2
src/ar/teamx/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
12
src/ar/teamx/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Team X'
|
||||
pkgNameSuffix = 'ar.teamx'
|
||||
extClass = '.TeamX'
|
||||
extVersionCode = 16
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
BIN
src/ar/teamx/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/ar/teamx/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/ar/teamx/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
src/ar/teamx/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/ar/teamx/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src/ar/teamx/res/web_hi_res_512.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
199
src/ar/teamx/src/eu/kanade/tachiyomi/extension/ar/teamx/TeamX.kt
Normal file
@@ -0,0 +1,199 @@
|
||||
package eu.kanade.tachiyomi.extension.ar.teamx
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TeamX : ParsedHttpSource() {
|
||||
|
||||
override val name = "Team X"
|
||||
|
||||
override val baseUrl = "https://team1x12.com"
|
||||
|
||||
override val lang = "ar"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.rateLimit(10, 1, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/series/" + if (page > 1) "?page=$page" else "", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.listupd div.bsx"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
title = element.select("a").attr("title")
|
||||
setUrlWithoutDomain(element.select("a").first()!!.attr("href"))
|
||||
thumbnail_url = element.select("img").let {
|
||||
if (it.hasAttr("data-src")) {
|
||||
it.attr("abs:data-src")
|
||||
} else {
|
||||
it.attr("abs:src")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a[rel=next]"
|
||||
|
||||
// Latest
|
||||
|
||||
private val titlesAdded = mutableSetOf<String>()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
if (page == 1) titlesAdded.clear()
|
||||
|
||||
return GET(baseUrl + if (page > 1) "?page=$page" else "", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val unfilteredManga = document.select(latestUpdatesSelector())
|
||||
|
||||
val mangaList = unfilteredManga.map { element ->
|
||||
latestUpdatesFromElement(element)
|
||||
}.distinctBy {
|
||||
it.title
|
||||
}.filter {
|
||||
!titlesAdded.contains(it.title)
|
||||
}
|
||||
|
||||
titlesAdded.addAll(mangaList.map { it.title })
|
||||
|
||||
return MangasPage(mangaList, document.select(latestUpdatesNextPageSelector()).isNotEmpty())
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "div.last-chapter div.box"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
val linkElement = element.select("div.info a")
|
||||
title = linkElement.select("h3").text()
|
||||
setUrlWithoutDomain(linkElement.first()!!.attr("href"))
|
||||
thumbnail_url = element.select("div.imgu img").first()!!.absUrl("src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/ajax/search?keyword=$query", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "li.list-group-item"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
val urlAndText = element.select("div.ms-2 a")
|
||||
title = urlAndText.text()
|
||||
setUrlWithoutDomain(urlAndText.first()!!.absUrl("href"))
|
||||
thumbnail_url = element.select("a img").first()!!.absUrl("src")
|
||||
}
|
||||
}
|
||||
|
||||
// doesnt matter as there is no next page
|
||||
override fun searchMangaNextPageSelector(): String? = null
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
title = document.select("div.author-info-title h1").text()
|
||||
description = document.select("div.review-content").text()
|
||||
if (description.isNullOrBlank()) {
|
||||
description = document.select("div.review-content p").text()
|
||||
}
|
||||
genre = document.select("div.review-author-info a").joinToString { it.text() }
|
||||
thumbnail_url = document.select("div.text-right img").first()!!.absUrl("src")
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
private fun chapterNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val allElements = mutableListOf<Element>()
|
||||
var document = response.asJsoup()
|
||||
|
||||
while (true) {
|
||||
val pageChapters = document.select(chapterListSelector())
|
||||
if (pageChapters.isEmpty()) {
|
||||
break
|
||||
}
|
||||
|
||||
allElements += pageChapters
|
||||
|
||||
val hasNextPage = document.select(chapterNextPageSelector()).isNotEmpty()
|
||||
if (!hasNextPage) {
|
||||
break
|
||||
}
|
||||
|
||||
val nextUrl = document.select(chapterNextPageSelector()).attr("href")
|
||||
|
||||
document = client.newCall(GET(nextUrl, headers)).execute().asJsoup()
|
||||
}
|
||||
|
||||
return allElements.map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
private val chapterFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss", Locale.getDefault())
|
||||
|
||||
override fun chapterListSelector() = "div.eplister ul a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
val chpNum = element.select("div.epl-num").text()
|
||||
val chpTitle = element.select("div.epl-title").text()
|
||||
|
||||
name = when (chpNum.isNullOrBlank()) {
|
||||
true -> chpTitle
|
||||
false -> "$chpNum - $chpTitle"
|
||||
}
|
||||
|
||||
date_upload = parseChapterDate(element.select("div.epl-date").text())
|
||||
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return runCatching {
|
||||
chapterFormat.parse(date)?.time
|
||||
}.getOrNull() ?: 0
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div.image_list img[src]").mapIndexed { i, img ->
|
||||
Page(i, "", img.absUrl("src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
}
|
||||