From 73cab624d2ed5a94c4f4b2dca28058015143bbbb Mon Sep 17 00:00:00 2001 From: yemen Date: Thu, 9 Jul 2026 16:38:26 +0300 Subject: [PATCH 01/24] feat(ar): add MangaCloud source --- src/ar/mangacloud/build.gradle.kts | 16 ++ .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4201 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2325 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5884 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 11653 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 21204 bytes .../tachiyomi/extension/ar/mangacloud/Dto.kt | 118 ++++++++++ .../extension/ar/mangacloud/Filters.kt | 15 ++ .../extension/ar/mangacloud/MangaCloud.kt | 217 ++++++++++++++++++ 9 files changed, 366 insertions(+) create mode 100644 src/ar/mangacloud/build.gradle.kts create mode 100644 src/ar/mangacloud/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/ar/mangacloud/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/ar/mangacloud/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/ar/mangacloud/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/ar/mangacloud/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt create mode 100644 src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt create mode 100644 src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt diff --git a/src/ar/mangacloud/build.gradle.kts b/src/ar/mangacloud/build.gradle.kts new file mode 100644 index 00000000000..105982aa847 --- /dev/null +++ b/src/ar/mangacloud/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(kei.plugins.extension) +} + +keiyoushi { + name = "MangaCloud" + versionCode = 1 + contentWarning = ContentWarning.SAFE + libVersion = "1.4" + + source { + lang = "ar" + baseUrl = "https://mangacloud.online" + id = 4294967295 + } +} diff --git a/src/ar/mangacloud/res/mipmap-hdpi/ic_launcher.png b/src/ar/mangacloud/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ee1d29f8677c497949d785a9efc17969796c47c1 GIT binary patch literal 4201 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 = emptyList(), + val genres: List = emptyList(), + val statusString: String = "", +) + +data class MangaListResult( + val mangas: List, + val nextPageToken: String?, +) + +object FirestoreParser { + + fun parseList(response: Response): MangaListResult { + val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject + val documents = json["documents"]?.jsonArray ?: return MangaListResult(emptyList(), null) + val mangas = documents.mapNotNull { doc -> + val fields = doc.jsonObject["fields"]?.jsonObject ?: return@mapNotNull null + val mangaId = doc.jsonObject["name"]?.jsonPrimitive?.contentOrNull?.substringAfterLast("/") ?: return@mapNotNull null + val altTitles = fields.stringArray("alternativeTitles") + val genres = fields.stringArray("genres") + val statusStr = fields.string("status") + MangaData( + smanga = SManga.create().apply { + url = "/manga/$mangaId" + title = fields.string("title") + thumbnail_url = fields.string("coverImage") + description = fields.string("description") + author = fields.string("author") + artist = fields.string("artist") + genre = genres.joinToString(", ") + status = when (statusStr) { + "مستمر" -> SManga.ONGOING + "مكتمل" -> SManga.COMPLETED + "متروك", "متروكة" -> SManga.ON_HIATUS + "معلق" -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + }, + altTitles = altTitles, + genres = genres, + statusString = when (statusStr) { + "مستمر" -> "ongoing" + "مكتمل" -> "completed" + else -> "" + }, + ) + } + val nextPageToken = json["nextPageToken"]?.jsonPrimitive?.contentOrNull + return MangaListResult(mangas, nextPageToken) + } + + fun parseDetails(response: Response): SManga { + val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject + val fields = json["fields"]?.jsonObject ?: return SManga.create() + val mangaId = json["name"]?.jsonPrimitive?.contentOrNull?.substringAfterLast("/") ?: "" + return SManga.create().apply { + url = "/manga/$mangaId" + title = fields.string("title") + thumbnail_url = fields.string("coverImage") + description = fields.string("description") + author = fields.string("author") + artist = fields.string("artist") + genre = fields.stringArray("genres").joinToString(", ") + status = when (fields.string("status")) { + "مستمر" -> SManga.ONGOING + "مكتمل" -> SManga.COMPLETED + "متروك" -> SManga.ON_HIATUS + else -> SManga.UNKNOWN + } + } + } + + fun parseChapters(response: Response): List { + val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject + val documents = json["documents"]?.jsonArray ?: return emptyList() + return documents.mapNotNull { doc -> + val fields = doc.jsonObject["fields"]?.jsonObject ?: return@mapNotNull null + val docName = doc.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null + val chapterNum = docName.substringAfterLast("/") + val num = chapterNum.toIntOrNull() ?: 0 + SChapter.create().apply { + url = docName.substringAfterLast("cloudmangas/") + name = "الفصل $num" + chapter_number = num.toFloat() + } + }.sortedByDescending { it.chapter_number } + } + + fun parsePages(response: Response): List { + val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject + val fields = json["fields"]?.jsonObject ?: return emptyList() + val pagesArray = fields["pages"]?.jsonObject?.get("arrayValue")?.jsonObject?.get("values")?.jsonArray ?: return emptyList() + return pagesArray.mapIndexed { index, value -> + Page(index, imageUrl = value.jsonObject["stringValue"]?.jsonPrimitive?.contentOrNull ?: "") + } + } +} + +private fun JsonObject.string(key: String): String = this[key]?.jsonObject?.get("stringValue")?.jsonPrimitive?.contentOrNull ?: "" + +private fun JsonObject.stringArray(key: String): List = this[key]?.jsonObject?.get("arrayValue")?.jsonObject?.get("values")?.jsonArray + ?.map { it.jsonObject["stringValue"]?.jsonPrimitive?.contentOrNull ?: "" } + ?: emptyList() diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt new file mode 100644 index 00000000000..c73360fce80 --- /dev/null +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt @@ -0,0 +1,15 @@ +package eu.kanade.tachiyomi.extension.ar.mangacloud + +import eu.kanade.tachiyomi.source.model.Filter + +internal class SortFilter(name: String, private val pairs: Array>) : Filter.Select(name, pairs.map { it.first }.toTypedArray()) { + val selected get() = pairs[state].second +} + +internal class GenreFilter(name: String, private val pairs: Array>) : Filter.Select(name, pairs.map { it.first }.toTypedArray()) { + val selected get() = pairs[state].second +} + +internal class StatusFilter(name: String, private val pairs: Array>) : Filter.Select(name, pairs.map { it.first }.toTypedArray()) { + val selected get() = pairs[state].second +} diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt new file mode 100644 index 00000000000..22556922db6 --- /dev/null +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -0,0 +1,217 @@ +package eu.kanade.tachiyomi.extension.ar.mangacloud + +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.HttpSource +import keiyoushi.annotation.Source +import keiyoushi.utils.firstInstanceOrNull +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response + +@Source +abstract class MangaCloud : HttpSource() { + + override val supportsLatest = true + + override fun popularMangaRequest(page: Int): Request { + val url = FIRESTORE_URL.toHttpUrl().newBuilder() + .addQueryParameter("pageSize", PAGE_SIZE.toString()) + .addQueryParameter("key", API_KEY) + .apply { + val token = lastPageToken + if (token != null) addQueryParameter("pageToken", token) + } + .build() + return GET(url, headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val json = FirestoreParser.parseList(response) + lastPageToken = json.nextPageToken + return MangasPage(json.mangas.map { it.smanga }, json.nextPageToken != null) + } + + override fun latestUpdatesRequest(page: Int): Request { + val url = FIRESTORE_URL.toHttpUrl().newBuilder() + .addQueryParameter("pageSize", PAGE_SIZE.toString()) + .addQueryParameter("key", API_KEY) + .addQueryParameter("orderBy", "updatedAt desc") + .build() + return GET(url, headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val json = FirestoreParser.parseList(response) + return MangasPage(json.mangas.map { it.smanga }, false) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + currentSearchQuery = query + currentSortFilter = filters.firstInstanceOrNull()?.selected.orEmpty() + currentGenreFilter = filters.firstInstanceOrNull()?.selected.orEmpty() + currentStatusFilter = filters.firstInstanceOrNull()?.selected.orEmpty() + val url = FIRESTORE_URL.toHttpUrl().newBuilder() + .addQueryParameter("pageSize", "300") + .addQueryParameter("key", API_KEY) + .apply { + when (currentSortFilter) { + "new" -> addQueryParameter("orderBy", "createdAt desc") + "updated" -> addQueryParameter("orderBy", "updatedAt desc") + } + } + .build() + return GET(url, headers) + } + + override fun searchMangaParse(response: Response): MangasPage { + val query = currentSearchQuery.trim() + val genre = currentGenreFilter + val status = currentStatusFilter + val json = FirestoreParser.parseList(response) + val filtered = json.mangas.filter { data -> + val matchesQuery = if (query.isNotBlank()) { + val q = query.lowercase() + data.smanga.title.lowercase().contains(q) || + data.altTitles.any { it.lowercase().contains(q) } || + data.genres.any { it.lowercase().contains(q) } || + data.smanga.description?.lowercase()?.contains(q) == true + } else { + true + } + val matchesGenre = if (genre.isNotBlank()) { + data.genres.any { it.lowercase().contains(genre.lowercase()) } + } else { + true + } + val matchesStatus = if (status.isNotBlank()) { + data.statusString == status + } else { + true + } + matchesQuery && matchesGenre && matchesStatus + } + return MangasPage(filtered.map { it.smanga }, false) + } + + override fun mangaDetailsRequest(manga: SManga): Request { + val mangaId = manga.url.substringAfterLast("/") + val url = "$FIRESTORE_URL/$mangaId?key=$API_KEY".toHttpUrl() + return GET(url, headers) + } + + override fun mangaDetailsParse(response: Response): SManga = FirestoreParser.parseDetails(response) + + override fun chapterListRequest(manga: SManga): Request { + val mangaId = manga.url.substringAfterLast("/") + val url = "$FIRESTORE_URL/$mangaId/chapters?pageSize=1000&key=$API_KEY&orderBy=index".toHttpUrl() + return GET(url, headers) + } + + override fun chapterListParse(response: Response): List = FirestoreParser.parseChapters(response) + + override fun pageListRequest(chapter: SChapter): Request { + val url = "${FIRESTORE_URL}/${chapter.url}?key=$API_KEY".toHttpUrl() + return GET(url, headers) + } + + override fun pageListParse(response: Response): List = FirestoreParser.parsePages(response) + + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + + override fun getMangaUrl(manga: SManga): String { + val mangaId = manga.url.substringAfterLast("/") + return "$baseUrl/manga/$mangaId" + } + + override fun getChapterUrl(chapter: SChapter): String { + val parts = chapter.url.split("/") + if (parts.size >= 2) { + return "$baseUrl/manga/${parts[0]}/${parts[1]}" + } + return "$baseUrl/manga/${chapter.url}" + } + + override fun getFilterList(): FilterList = FilterList( + SortFilter("ترتيب حسب", sortingList), + StatusFilter("الحالة", statusList), + GenreFilter("التصنيف", genreList), + ) + + private var lastPageToken: String? = null + private var currentSearchQuery: String = "" + private var currentSortFilter: String = "" + private var currentGenreFilter: String = "" + private var currentStatusFilter: String = "" + + companion object { + private const val API_KEY = "AIzaSyAvdmgz_r_d89Eo8JBs9vAjUAJR451bMYU" + private const val FIRESTORE_URL = "https://firestore.googleapis.com/v1/projects/ninja-brew-crew-4d79c/databases/(default)/documents/cloudmangas" + private const val PAGE_SIZE = 20 + + private val sortingList = arrayOf( + Pair("الأشهر", "popular"), + Pair("آخر التحديثات", "updated"), + Pair("إضافات جديدة", "new"), + ) + + private val statusList = arrayOf( + Pair("الكل", ""), + Pair("مستمر", "ongoing"), + Pair("مكتمل", "completed"), + ) + + private val genreList = arrayOf( + Pair("الكل", ""), + Pair("أكشن", "أكشن"), + Pair("إعادة احياء", "إعادة احياء"), + Pair("إيسيكاي", "إيسيكاي"), + Pair("الفنون القتالية", "الفنون القتالية"), + Pair("اتصال بالعالم الخارجي", "اتصال بالعالم الخارجي"), + Pair("اثاره", "اثاره"), + Pair("اسلحة خارقة", "اسلحة خارقة"), + Pair("اسطوره", "اسطوره"), + Pair("اميرة", "اميرة"), + Pair("بزنس", "بزنس"), + Pair("بطل غير اعتيادي", "بطل غير اعتيادي"), + Pair("تبديل عوالم", "تبديل عوالم"), + Pair("تجسيد", "تجسيد"), + Pair("حريم", "حريم"), + Pair("حياة مدرسية", "حياة مدرسية"), + Pair("خوارق", "خوارق"), + Pair("خيال", "خيال"), + Pair("خيال علمي", "خيال علمي"), + Pair("دراما", "دراما"), + Pair("رعب", "رعب"), + Pair("رومانسي", "رومانسي"), + Pair("زومبي", "زومبي"), + Pair("سحر", "سحر"), + Pair("شريحة من الحياة", "شريحة من الحياة"), + Pair("شونين", "شونين"), + Pair("شياطين", "شياطين"), + Pair("عصابات", "عصابات"), + Pair("غموض", "غموض"), + Pair("فتى خارق", "فتى خارق"), + Pair("فنتازيا", "فنتازيا"), + Pair("فنون قتاليه", "فنون قتاليه"), + Pair("قراصنة", "قراصنة"), + Pair("كوميدي", "كوميدي"), + Pair("كوميديا", "كوميديا"), + Pair("مانهوا", "مانهوا"), + Pair("مانها", "مانها"), + Pair("مانجا", "مانجا"), + Pair("مغامرة", "مغامرة"), + Pair("موسيقى", "موسيقى"), + Pair("نظام", "نظام"), + Pair("نفسي", "نفسي"), + Pair("نهاية العالم", "نهاية العالم"), + Pair("وحوش", "وحوش"), + Pair("ويبتون", "ويبتون"), + Pair("ون شوت", "ون شوت"), + ) + } +} From baf9ebc61052441c05f8316d9f4a0b34b4324de4 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 08:44:48 +0300 Subject: [PATCH 02/24] Update build.gradle.kts --- src/ar/mangacloud/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ar/mangacloud/build.gradle.kts b/src/ar/mangacloud/build.gradle.kts index 105982aa847..fb78d82d6c6 100644 --- a/src/ar/mangacloud/build.gradle.kts +++ b/src/ar/mangacloud/build.gradle.kts @@ -11,6 +11,5 @@ keiyoushi { source { lang = "ar" baseUrl = "https://mangacloud.online" - id = 4294967295 } } From 79fc155210917af90086697890c4fba3776ab5cd Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 14:09:54 +0300 Subject: [PATCH 03/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 22556922db6..ffe9bfa7446 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -12,6 +12,8 @@ import keiyoushi.utils.firstInstanceOrNull import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import org.json.JSONObject @Source abstract class MangaCloud : HttpSource() { @@ -112,7 +114,48 @@ abstract class MangaCloud : HttpSource() { return GET(url, headers) } - override fun chapterListParse(response: Response): List = FirestoreParser.parseChapters(response) + override fun chapterListParse(response: Response): List { + val allChapters = mutableListOf() + var nextPageToken: String? = null + var currentResponse = response + + do { + // قراءة محتوى الاستجابة الحالية كـ JSON + val jsonString = currentResponse.body?.string() ?: break + val json = JSONObject(jsonString) + + // إعادة بناء استجابة جديدة بنفس المحتوى لتمريرها إلى FirestoreParser.parseChapters + val newResponse = currentResponse.newBuilder() + .body(jsonString.toResponseBody(null)) + .build() + val chapters = FirestoreParser.parseChapters(newResponse) + allChapters.addAll(chapters) + + // استخراج nextPageToken من JSON + nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } + + if (nextPageToken != null) { + // استخراج mangaId من مسار الطلب الأصلي + val pathSegments = currentResponse.request.url.pathSegments + val chaptersIndex = pathSegments.indexOf("chapters") + if (chaptersIndex < 1) break + val mangaId = pathSegments[chaptersIndex - 1] + + // بناء طلب الصفحة التالية + val urlBuilder = "$FIRESTORE_URL/$mangaId/chapters".toHttpUrl().newBuilder() + .addQueryParameter("pageSize", "1000") + .addQueryParameter("key", API_KEY) + .addQueryParameter("orderBy", "index") + .addQueryParameter("pageToken", nextPageToken) + + val newRequest = GET(urlBuilder.build(), headers) + currentResponse = client.newCall(newRequest).execute() + } + + } while (nextPageToken != null && currentResponse.isSuccessful) + + return allChapters + } override fun pageListRequest(chapter: SChapter): Request { val url = "${FIRESTORE_URL}/${chapter.url}?key=$API_KEY".toHttpUrl() From 18598ef311d33f1402a836e920fa29720709c67d Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 14:20:53 +0300 Subject: [PATCH 04/24] Update MangaCloud.kt --- .../kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index ffe9bfa7446..5f3ff19c164 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -120,28 +120,23 @@ abstract class MangaCloud : HttpSource() { var currentResponse = response do { - // قراءة محتوى الاستجابة الحالية كـ JSON val jsonString = currentResponse.body?.string() ?: break val json = JSONObject(jsonString) - // إعادة بناء استجابة جديدة بنفس المحتوى لتمريرها إلى FirestoreParser.parseChapters val newResponse = currentResponse.newBuilder() .body(jsonString.toResponseBody(null)) .build() val chapters = FirestoreParser.parseChapters(newResponse) allChapters.addAll(chapters) - // استخراج nextPageToken من JSON nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } if (nextPageToken != null) { - // استخراج mangaId من مسار الطلب الأصلي val pathSegments = currentResponse.request.url.pathSegments val chaptersIndex = pathSegments.indexOf("chapters") if (chaptersIndex < 1) break val mangaId = pathSegments[chaptersIndex - 1] - // بناء طلب الصفحة التالية val urlBuilder = "$FIRESTORE_URL/$mangaId/chapters".toHttpUrl().newBuilder() .addQueryParameter("pageSize", "1000") .addQueryParameter("key", API_KEY) @@ -151,7 +146,6 @@ abstract class MangaCloud : HttpSource() { val newRequest = GET(urlBuilder.build(), headers) currentResponse = client.newCall(newRequest).execute() } - } while (nextPageToken != null && currentResponse.isSuccessful) return allChapters From 84ffac963e4ace4c4f2ed485d6494287849a6b7f Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 14:46:18 +0300 Subject: [PATCH 05/24] Update MangaCloud.kt --- .../eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 5f3ff19c164..cbd0bdc6a70 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -148,6 +148,9 @@ abstract class MangaCloud : HttpSource() { } } while (nextPageToken != null && currentResponse.isSuccessful) + // ترتيب الفصول تنازلياً (الأحدث أولاً) حسب رقم الفصل + allChapters.sortByDescending { it.chapter_number?.toFloatOrNull() ?: 0f } + return allChapters } From e462fbac435c4463a45ed09c9d26f3d3fc86c528 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 14:52:39 +0300 Subject: [PATCH 06/24] Update MangaCloud.kt --- .../eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index cbd0bdc6a70..b010c033383 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -149,7 +149,9 @@ abstract class MangaCloud : HttpSource() { } while (nextPageToken != null && currentResponse.isSuccessful) // ترتيب الفصول تنازلياً (الأحدث أولاً) حسب رقم الفصل - allChapters.sortByDescending { it.chapter_number?.toFloatOrNull() ?: 0f } + allChapters.sortByDescending { + it.chapter_number?.toString()?.toFloatOrNull() ?: 0f + } return allChapters } From 902681208097dbe1eb290ab0936201e90b81a5db Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:10:53 +0300 Subject: [PATCH 07/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 219 ++++++++++++++++-- 1 file changed, 204 insertions(+), 15 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index b010c033383..3475bc8218c 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -148,7 +148,6 @@ abstract class MangaCloud : HttpSource() { } } while (nextPageToken != null && currentResponse.isSuccessful) - // ترتيب الفصول تنازلياً (الأحدث أولاً) حسب رقم الفصل allChapters.sortByDescending { it.chapter_number?.toString()?.toFloatOrNull() ?: 0f } @@ -209,51 +208,241 @@ abstract class MangaCloud : HttpSource() { private val genreList = arrayOf( Pair("الكل", ""), - Pair("أكشن", "أكشن"), + // التصنيفات الأصلية + الجديدة (مرتبة أبجدياً، بدون تكرار) + Pair("إدارة المناطق", "إدارة المناطق"), Pair("إعادة احياء", "إعادة احياء"), + Pair("إعادة بحث", "إعادة بحث"), + Pair("إنتقام", "إنتقام"), Pair("إيسيكاي", "إيسيكاي"), - Pair("الفنون القتالية", "الفنون القتالية"), - Pair("اتصال بالعالم الخارجي", "اتصال بالعالم الخارجي"), + Pair("ابراج", "ابراج"), Pair("اثاره", "اثاره"), - Pair("اسلحة خارقة", "اسلحة خارقة"), - Pair("اسطوره", "اسطوره"), + Pair("اتصال بالعالم الخارجي", "اتصال بالعالم الخارجي"), + Pair("اجناس", "اجناس"), + Pair("احتجاج", "احتجاج"), + Pair("اختفاء", "اختفاء"), + Pair("ارتقاء", "ارتقاء"), + Pair("ارواح", "ارواح"), + Pair("ازياء", "ازياء"), + Pair("اساطير", "اساطير"), + Pair("اساطيز", "اساطيز"), + Pair("اسبوعى", "اسبوعى"), + Pair("استراتيجية", "استراتيجية"), + Pair("اشباح", "اشباح"), + Pair("اضطهاد", "اضطهاد"), + Pair("اظطهاد", "اظطهاد"), + Pair("اعادة احياء", "اعادة احياء"), + Pair("اعمار", "اعمار"), + Pair("اعمال", "اعمال"), + Pair("اقتصاد", "اقتصاد"), + Pair("اكاديميه", "اكاديميه"), + Pair("اكشن", "اكشن"), + Pair("الات", "الات"), + Pair("الالوان الممتلئه", "الالوان الممتلئه"), + Pair("البقاء علي قيد الحياه", "البقاء علي قيد الحياه"), + Pair("الجانب المظلم من الحياه", "الجانب المظلم من الحياه"), + Pair("الحريم العكسي", "الحريم العكسي"), + Pair("الحياة المدرسيه", "الحياة المدرسيه"), + Pair("الحياة اليومية", "الحياة اليومية"), + Pair("الحيوانات الأليفة", "الحيوانات الأليفة"), + Pair("الخيال العلمي", "الخيال العلمي"), + Pair("السفر عبر الزمن", "السفر عبر الزمن"), + Pair("العاب", "العاب"), + Pair("العاب الكترونية", "العاب الكترونية"), + Pair("العاب تقليدية", "العاب تقليدية"), + Pair("العاب رعب", "العاب رعب"), + Pair("العاب فيديو", "العاب فيديو"), + Pair("العصور الوسطى", "العصور الوسطى"), + Pair("الغموض", "الغموض"), + Pair("الفتاة الوحش", "الفتاة الوحش"), + Pair("الفنون القتالية", "الفنون القتالية"), + Pair("الفنون العسكرية", "الفنون العسكرية"), + Pair("المخالفون للقانون", "المخالفون للقانون"), + Pair("النجاة", "النجاة"), + Pair("الهة", "الهة"), + Pair("الهه", "الهه"), + Pair("الواقع الافتراضي", "الواقع الافتراضي"), + Pair("اليات", "اليات"), + Pair("امرأة شريرة", "امرأة شريرة"), Pair("اميرة", "اميرة"), + Pair("انتقال", "انتقال"), + Pair("انمى", "انمى"), + Pair("ايتشى", "ايتشى"), + Pair("ايسكاى", "ايسكاى"), + Pair("ايشي", "ايشي"), + Pair("بالغ", "بالغ"), Pair("بزنس", "بزنس"), + Pair("بطل خارق", "بطل خارق"), Pair("بطل غير اعتيادي", "بطل غير اعتيادي"), - Pair("تبديل عوالم", "تبديل عوالم"), + Pair("بطل غير اعتيادى", "بطل غير اعتيادى"), + Pair("بطل مجنون", "بطل مجنون"), + Pair("بطل وحش", "بطل وحش"), + Pair("بعد الكارثه", "بعد الكارثه"), + Pair("بوليسي", "بوليسي"), + Pair("تاريخ", "تاريخ"), + Pair("تاريخى", "تاريخى"), + Pair("تجاره", "تجاره"), Pair("تجسيد", "تجسيد"), + Pair("تحديث", "تحديث"), + Pair("تحري", "تحري"), + Pair("تحقيق", "تحقيق"), + Pair("تحقيقات", "تحقيقات"), + Pair("تخطيط", "تخطيط"), + Pair("تدريب", "تدريب"), + Pair("تراجع", "تراجع"), + Pair("تراجيدي", "تراجيدي"), + Pair("ترويض", "ترويض"), + Pair("ترويض وحوش", "ترويض وحوش"), + Pair("تشويق", "تشويق"), + Pair("تلوين رسم", "تلوين رسم"), + Pair("تلوين رسمي", "تلوين رسمي"), + Pair("تلوين هواة", "تلوين هواة"), + Pair("تملك", "تملك"), + Pair("تناسخ", "تناسخ"), + Pair("تناسخ الارواح", "تناسخ الارواح"), + Pair("تنانين", "تنانين"), + Pair("تنايخ", "تنايخ"), + Pair("ثأر", "ثأر"), + Pair("جانحون", "جانحون"), + Pair("جريمة", "جريمة"), + Pair("جريمه", "جريمه"), + Pair("جندر اسواب", "جندر اسواب"), + Pair("جندر بندير", "جندر بندير"), + Pair("جوسي", "جوسي"), + Pair("جوسين", "جوسين"), + Pair("جوسيه", "جوسيه"), + Pair("حائز علي جائزة", "حائز علي جائزة"), + Pair("حديث", "حديث"), + Pair("حرب", "حرب"), + Pair("حربي", "حربي"), Pair("حريم", "حريم"), + Pair("حريم عكسي", "حريم عكسي"), + Pair("حصرية", "حصرية"), Pair("حياة مدرسية", "حياة مدرسية"), - Pair("خوارق", "خوارق"), + Pair("حياة يومية", "حياة يومية"), + Pair("حيوانات", "حيوانات"), + Pair("حيوانات اليفه", "حيوانات اليفه"), + Pair("خارق", "خارق"), + Pair("خارق للطبيعه", "خارق للطبيعه"), + Pair("خيار", "خيار"), Pair("خيال", "خيال"), Pair("خيال علمي", "خيال علمي"), + Pair("خيالي", "خيالي"), + Pair("داخل اللعبه", "داخل اللعبه"), + Pair("داخل روايه", "داخل روايه"), Pair("دراما", "دراما"), + Pair("دماء", "دماء"), + Pair("دموى", "دموى"), + Pair("ذكريات من عالم آخر", "ذكريات من عالم آخر"), + Pair("راشد", "راشد"), + Pair("رعاية اطفال", "رعاية اطفال"), Pair("رعب", "رعب"), + Pair("روايات عربية", "روايات عربية"), + Pair("روايه", "روايه"), Pair("رومانسي", "رومانسي"), + Pair("رياضه", "رياضه"), + Pair("رياضي", "رياضي"), + Pair("زراعة", "زراعة"), + Pair("زمكاني", "زمكاني"), + Pair("زمنكاني", "زمنكاني"), + Pair("زنزانات", "زنزانات"), + Pair("زواج مدبر", "زواج مدبر"), Pair("زومبي", "زومبي"), + Pair("ساموراي", "ساموراي"), + Pair("ساموري", "ساموري"), + Pair("سايكوباث", "سايكوباث"), Pair("سحر", "سحر"), + Pair("سيرة ذاتية", "سيرة ذاتية"), + Pair("سياسي", "سياسي"), + Pair("سينين", "سينين"), + Pair("شرطة", "شرطة"), + Pair("شركه", "شركه"), Pair("شريحة من الحياة", "شريحة من الحياة"), + Pair("شرير", "شرير"), + Pair("شوجو", "شوجو"), Pair("شونين", "شونين"), Pair("شياطين", "شياطين"), - Pair("عصابات", "عصابات"), + Pair("شينين", "شينين"), + Pair("صقل", "صقل"), + Pair("طبخ", "طبخ"), + Pair("طبي", "طبي"), + Pair("طرد الارواح الشريره", "طرد الارواح الشريره"), + Pair("عائلي", "عائلي"), + Pair("عالم مختلف", "عالم مختلف"), + Pair("عامل مكتبي", "عامل مكتبي"), + Pair("عسكري", "عسكري"), + Pair("عسكريه", "عسكريه"), + Pair("عصر حديث", "عصر حديث"), + Pair("عصور وسطى", "عصور وسطى"), + Pair("علم نفس", "علم نفس"), + Pair("علمي", "علمي"), + Pair("عنف", "عنف"), + Pair("عنف جنسي", "عنف جنسي"), + Pair("عوالم", "عوالم"), + Pair("عوده بالزمن", "عوده بالزمن"), Pair("غموض", "غموض"), - Pair("فتى خارق", "فتى خارق"), + Pair("فانتازا", "فانتازا"), + Pair("فانتازيا", "فانتازيا"), + Pair("فانتسي", "فانتسي"), + Pair("فلسفه", "فلسفه"), Pair("فنتازيا", "فنتازيا"), + Pair("فنون الدفاع عن النفس", "فنون الدفاع عن النفس"), Pair("فنون قتاليه", "فنون قتاليه"), - Pair("قراصنة", "قراصنة"), - Pair("كوميدي", "كوميدي"), + Pair("فوق الطبيعه", "فوق الطبيعه"), + Pair("فيكتوري", "فيكتوري"), + Pair("قتال", "قتال"), + Pair("قتالات", "قتالات"), + Pair("قوة خارقة", "قوة خارقة"), + Pair("كائنات فضائية", "كائنات فضائية"), + Pair("كل الاعمار", "كل الاعمار"), Pair("كوميديا", "كوميديا"), - Pair("مانهوا", "مانهوا"), - Pair("مانها", "مانها"), + Pair("كوميدى", "كوميدى"), + Pair("كوميك", "كوميك"), + Pair("كوما-4", "كوما-4"), + Pair("لعبه", "لعبه"), + Pair("مأساوي", "مأساوي"), + Pair("مؤامرات", "مؤامرات"), + Pair("ما بعد نهاية العالم", "ما بعد نهاية العالم"), + Pair("مأساة", "مأساة"), + Pair("مافيا", "مافيا"), + Pair("مانا", "مانا"), Pair("مانجا", "مانجا"), + Pair("مانجا ملونه", "مانجا ملونه"), + Pair("مانها", "مانها"), + Pair("مانهوا", "مانهوا"), + Pair("متحول", "متحول"), + Pair("مجموعة قصص", "مجموعة قصص"), + Pair("مدرسه", "مدرسه"), + Pair("مدرسي", "مدرسي"), + Pair("مستذئب", "مستذئب"), + Pair("مصاصي الدماء", "مصاصي الدماء"), + Pair("معالج", "معالج"), Pair("مغامرة", "مغامرة"), + Pair("مغني", "مغني"), + Pair("مقتبسة", "مقتبسة"), + Pair("مقطع طولي", "مقطع طولي"), + Pair("ملائكة", "ملائكة"), + Pair("ملونه", "ملونه"), + Pair("ممالك", "ممالك"), + Pair("موريم", "موريم"), Pair("موسيقى", "موسيقى"), + Pair("ميكا", "ميكا"), + Pair("ناضج", "ناضج"), + Pair("نبالة", "نبالة"), + Pair("نبلاء", "نبلاء"), Pair("نظام", "نظام"), Pair("نفسي", "نفسي"), Pair("نهاية العالم", "نهاية العالم"), - Pair("وحوش", "وحوش"), + Pair("نينجا", "نينجا"), + Pair("هندسة", "هندسة"), + Pair("هواه", "هواه"), + Pair("هوس", "هوس"), + Pair("واقع افتراضي", "واقع افتراضي"), + Pair("واقعى", "واقعى"), Pair("ويبتون", "ويبتون"), + Pair("وحوش", "وحوش"), Pair("ون شوت", "ون شوت"), + Pair("ويب تون", "ويب تون"), + Pair("يقاء", "يقاء"), ) } } From 3e05ec570a5a7a2417d0fc1ed91b17538049a48d Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:22:05 +0300 Subject: [PATCH 08/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 51 +++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 3475bc8218c..92a6023db3d 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -53,10 +53,13 @@ abstract class MangaCloud : HttpSource() { } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + // حفظ الفلاتر للاستخدام في التحليل currentSearchQuery = query currentSortFilter = filters.firstInstanceOrNull()?.selected.orEmpty() currentGenreFilter = filters.firstInstanceOrNull()?.selected.orEmpty() currentStatusFilter = filters.firstInstanceOrNull()?.selected.orEmpty() + + // بناء الطلب الأساسي (الصفحة الأولى) val url = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", "300") .addQueryParameter("key", API_KEY) @@ -65,6 +68,7 @@ abstract class MangaCloud : HttpSource() { "new" -> addQueryParameter("orderBy", "createdAt desc") "updated" -> addQueryParameter("orderBy", "updatedAt desc") } + // لا نضيف pageToken هنا، سنتعامل معها في parse } .build() return GET(url, headers) @@ -74,8 +78,47 @@ abstract class MangaCloud : HttpSource() { val query = currentSearchQuery.trim() val genre = currentGenreFilter val status = currentStatusFilter - val json = FirestoreParser.parseList(response) - val filtered = json.mangas.filter { data -> + + // جلب جميع الصفحات بشكل متكرر + val allItems = mutableListOf() + var nextPageToken: String? = null + var currentResponse = response + + do { + // قراءة JSON من الاستجابة الحالية + val jsonString = currentResponse.body?.string() ?: break + val json = JSONObject(jsonString) + + // تحليل البيانات باستخدام FirestoreParser (نفترض أنه يعطي قائمة من FirestoreMangaData) + // لكن parseList يعيد كائن يحتوي على mangas و nextPageToken + // سنعيد استخدام parseList مع استجابة جديدة + val newResponse = currentResponse.newBuilder() + .body(jsonString.toResponseBody(null)) + .build() + val parsed = FirestoreParser.parseList(newResponse) + allItems.addAll(parsed.mangas) + + nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } + + if (nextPageToken != null) { + // بناء طلب الصفحة التالية مع pageToken + val urlBuilder = FIRESTORE_URL.toHttpUrl().newBuilder() + .addQueryParameter("pageSize", "300") + .addQueryParameter("key", API_KEY) + .addQueryParameter("pageToken", nextPageToken) + .apply { + when (currentSortFilter) { + "new" -> addQueryParameter("orderBy", "createdAt desc") + "updated" -> addQueryParameter("orderBy", "updatedAt desc") + } + } + val newRequest = GET(urlBuilder.build(), headers) + currentResponse = client.newCall(newRequest).execute() + } + } while (nextPageToken != null && currentResponse.isSuccessful) + + // الآن نقوم بالتصفية على جميع العناصر التي تم جلبها + val filtered = allItems.filter { data -> val matchesQuery = if (query.isNotBlank()) { val q = query.lowercase() data.smanga.title.lowercase().contains(q) || @@ -97,6 +140,8 @@ abstract class MangaCloud : HttpSource() { } matchesQuery && matchesGenre && matchesStatus } + + // نعيد النتائج مع hasNextPage = false لأننا جلبنا الكل return MangasPage(filtered.map { it.smanga }, false) } @@ -208,7 +253,7 @@ abstract class MangaCloud : HttpSource() { private val genreList = arrayOf( Pair("الكل", ""), - // التصنيفات الأصلية + الجديدة (مرتبة أبجدياً، بدون تكرار) + // جميع التصنيفات (مرتبة أبجدياً، بدون تكرار) Pair("إدارة المناطق", "إدارة المناطق"), Pair("إعادة احياء", "إعادة احياء"), Pair("إعادة بحث", "إعادة بحث"), From a3e4d26cbeff38cee98284c3ba7e2d542a252c3b Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:29:10 +0300 Subject: [PATCH 09/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 92a6023db3d..619a61bb9e1 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -15,6 +15,10 @@ import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import org.json.JSONObject +// استيراد الفئات الداخلية من FirestoreParser (يفترض وجودها) +import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData +import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse + @Source abstract class MangaCloud : HttpSource() { @@ -53,13 +57,11 @@ abstract class MangaCloud : HttpSource() { } override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - // حفظ الفلاتر للاستخدام في التحليل currentSearchQuery = query currentSortFilter = filters.firstInstanceOrNull()?.selected.orEmpty() currentGenreFilter = filters.firstInstanceOrNull()?.selected.orEmpty() currentStatusFilter = filters.firstInstanceOrNull()?.selected.orEmpty() - // بناء الطلب الأساسي (الصفحة الأولى) val url = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", "300") .addQueryParameter("key", API_KEY) @@ -68,7 +70,6 @@ abstract class MangaCloud : HttpSource() { "new" -> addQueryParameter("orderBy", "createdAt desc") "updated" -> addQueryParameter("orderBy", "updatedAt desc") } - // لا نضيف pageToken هنا، سنتعامل معها في parse } .build() return GET(url, headers) @@ -79,19 +80,15 @@ abstract class MangaCloud : HttpSource() { val genre = currentGenreFilter val status = currentStatusFilter - // جلب جميع الصفحات بشكل متكرر + // جلب جميع الصفحات باستخدام FirestoreMangaData val allItems = mutableListOf() var nextPageToken: String? = null var currentResponse = response do { - // قراءة JSON من الاستجابة الحالية val jsonString = currentResponse.body?.string() ?: break val json = JSONObject(jsonString) - // تحليل البيانات باستخدام FirestoreParser (نفترض أنه يعطي قائمة من FirestoreMangaData) - // لكن parseList يعيد كائن يحتوي على mangas و nextPageToken - // سنعيد استخدام parseList مع استجابة جديدة val newResponse = currentResponse.newBuilder() .body(jsonString.toResponseBody(null)) .build() @@ -101,7 +98,6 @@ abstract class MangaCloud : HttpSource() { nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } if (nextPageToken != null) { - // بناء طلب الصفحة التالية مع pageToken val urlBuilder = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", "300") .addQueryParameter("key", API_KEY) @@ -117,7 +113,7 @@ abstract class MangaCloud : HttpSource() { } } while (nextPageToken != null && currentResponse.isSuccessful) - // الآن نقوم بالتصفية على جميع العناصر التي تم جلبها + // التصفية على جميع العناصر المجلوبة val filtered = allItems.filter { data -> val matchesQuery = if (query.isNotBlank()) { val q = query.lowercase() @@ -141,7 +137,6 @@ abstract class MangaCloud : HttpSource() { matchesQuery && matchesGenre && matchesStatus } - // نعيد النتائج مع hasNextPage = false لأننا جلبنا الكل return MangasPage(filtered.map { it.smanga }, false) } @@ -253,7 +248,6 @@ abstract class MangaCloud : HttpSource() { private val genreList = arrayOf( Pair("الكل", ""), - // جميع التصنيفات (مرتبة أبجدياً، بدون تكرار) Pair("إدارة المناطق", "إدارة المناطق"), Pair("إعادة احياء", "إعادة احياء"), Pair("إعادة بحث", "إعادة بحث"), From 7c4a06e77cb89e51495f6c5adcffd991b17ed22e Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:33:33 +0300 Subject: [PATCH 10/24] Update MangaCloud.kt --- .../kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 619a61bb9e1..a125c6aaf9a 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,5 +1,7 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud +import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse +import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -15,10 +17,6 @@ import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody import org.json.JSONObject -// استيراد الفئات الداخلية من FirestoreParser (يفترض وجودها) -import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData -import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse - @Source abstract class MangaCloud : HttpSource() { From 3d8c446ed32e329aee362f3308aed3b02d8be744 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:37:17 +0300 Subject: [PATCH 11/24] Update MangaCloud.kt --- .../eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index a125c6aaf9a..67678d5fe7d 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud -import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList From 088c2a337f72b4663390230f01385e7dfc875d8a Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:43:46 +0300 Subject: [PATCH 12/24] Update MangaCloud.kt --- .../eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 67678d5fe7d..086246e096a 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud +import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList @@ -77,7 +78,6 @@ abstract class MangaCloud : HttpSource() { val genre = currentGenreFilter val status = currentStatusFilter - // جلب جميع الصفحات باستخدام FirestoreMangaData val allItems = mutableListOf() var nextPageToken: String? = null var currentResponse = response @@ -110,7 +110,6 @@ abstract class MangaCloud : HttpSource() { } } while (nextPageToken != null && currentResponse.isSuccessful) - // التصفية على جميع العناصر المجلوبة val filtered = allItems.filter { data -> val matchesQuery = if (query.isNotBlank()) { val q = query.lowercase() From 5ef3b3e9f9ea5f800e7a4a6b5b95a02deddd2e57 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:48:01 +0300 Subject: [PATCH 13/24] Update MangaCloud.kt --- .../eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 086246e096a..15cb6e7701d 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud -import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList From 0509323faced03bc90b4a3e838c1780dc3f7029f Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:53:03 +0300 Subject: [PATCH 14/24] Update MangaCloud.kt --- .../eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 15cb6e7701d..82bc5398992 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud +import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList @@ -88,7 +89,7 @@ abstract class MangaCloud : HttpSource() { val newResponse = currentResponse.newBuilder() .body(jsonString.toResponseBody(null)) .build() - val parsed = FirestoreParser.parseList(newResponse) + val parsed: FirestoreListResponse = FirestoreParser.parseList(newResponse) allItems.addAll(parsed.mangas) nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } From b2401dab55fb07ff6e45070a5a3df9c57b024681 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 15:58:10 +0300 Subject: [PATCH 15/24] Update MangaCloud.kt --- .../kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 82bc5398992..04e4e0a22a7 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,7 +1,5 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud -import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreListResponse -import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser.FirestoreMangaData import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -78,7 +76,7 @@ abstract class MangaCloud : HttpSource() { val genre = currentGenreFilter val status = currentStatusFilter - val allItems = mutableListOf() + val allItems = mutableListOf() var nextPageToken: String? = null var currentResponse = response @@ -89,7 +87,7 @@ abstract class MangaCloud : HttpSource() { val newResponse = currentResponse.newBuilder() .body(jsonString.toResponseBody(null)) .build() - val parsed: FirestoreListResponse = FirestoreParser.parseList(newResponse) + val parsed: FirestoreParser.FirestoreListResponse = FirestoreParser.parseList(newResponse) allItems.addAll(parsed.mangas) nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } From 11758aae0de8f5ff5c6c9bd850e0a4d74a8729a8 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 16:04:07 +0300 Subject: [PATCH 16/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 04e4e0a22a7..1f47c4721a1 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -76,21 +76,18 @@ abstract class MangaCloud : HttpSource() { val genre = currentGenreFilter val status = currentStatusFilter - val allItems = mutableListOf() + val allItems = mutableListOf() var nextPageToken: String? = null var currentResponse = response do { val jsonString = currentResponse.body?.string() ?: break - val json = JSONObject(jsonString) - val newResponse = currentResponse.newBuilder() .body(jsonString.toResponseBody(null)) .build() - val parsed: FirestoreParser.FirestoreListResponse = FirestoreParser.parseList(newResponse) + val parsed = FirestoreParser.parseList(newResponse) allItems.addAll(parsed.mangas) - - nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } + nextPageToken = parsed.nextPageToken if (nextPageToken != null) { val urlBuilder = FIRESTORE_URL.toHttpUrl().newBuilder() @@ -108,30 +105,52 @@ abstract class MangaCloud : HttpSource() { } } while (nextPageToken != null && currentResponse.isSuccessful) + // استخراج الخصائص باستخدام الانعكاس val filtered = allItems.filter { data -> + val smanga = try { + data::class.members.find { it.name == "smanga" }?.call(data) as? SManga + } catch (_: Exception) { null } + val genres = try { + (data::class.members.find { it.name == "genres" }?.call(data) as? List<*>)?.mapNotNull { it.toString() } ?: emptyList() + } catch (_: Exception) { emptyList() } + val altTitles = try { + (data::class.members.find { it.name == "altTitles" }?.call(data) as? List<*>)?.mapNotNull { it.toString() } ?: emptyList() + } catch (_: Exception) { emptyList() } + val statusString = try { + data::class.members.find { it.name == "statusString" }?.call(data) as? String ?: "" + } catch (_: Exception) { "" } + + if (smanga == null) return@filter false + val matchesQuery = if (query.isNotBlank()) { val q = query.lowercase() - data.smanga.title.lowercase().contains(q) || - data.altTitles.any { it.lowercase().contains(q) } || - data.genres.any { it.lowercase().contains(q) } || - data.smanga.description?.lowercase()?.contains(q) == true + smanga.title.lowercase().contains(q) || + altTitles.any { it.lowercase().contains(q) } || + genres.any { it.lowercase().contains(q) } || + smanga.description?.lowercase()?.contains(q) == true } else { true } val matchesGenre = if (genre.isNotBlank()) { - data.genres.any { it.lowercase().contains(genre.lowercase()) } + genres.any { it.lowercase().contains(genre.lowercase()) } } else { true } val matchesStatus = if (status.isNotBlank()) { - data.statusString == status + statusString == status } else { true } matchesQuery && matchesGenre && matchesStatus } - return MangasPage(filtered.map { it.smanga }, false) + val mangas = filtered.mapNotNull { data -> + try { + data::class.members.find { it.name == "smanga" }?.call(data) as? SManga + } catch (_: Exception) { null } + } + + return MangasPage(mangas, false) } override fun mangaDetailsRequest(manga: SManga): Request { From 0576974726d6e6b9e0196babbb8cf94df0a8ddc8 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 16:07:44 +0300 Subject: [PATCH 17/24] Update MangaCloud.kt --- .../eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 1f47c4721a1..18ee082d144 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -105,7 +105,6 @@ abstract class MangaCloud : HttpSource() { } } while (nextPageToken != null && currentResponse.isSuccessful) - // استخراج الخصائص باستخدام الانعكاس val filtered = allItems.filter { data -> val smanga = try { data::class.members.find { it.name == "smanga" }?.call(data) as? SManga From e74a312706202477c1ba346bec99fc78846b5cd6 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 16:11:55 +0300 Subject: [PATCH 18/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 18ee082d144..ac184eb9007 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -108,16 +108,24 @@ abstract class MangaCloud : HttpSource() { val filtered = allItems.filter { data -> val smanga = try { data::class.members.find { it.name == "smanga" }?.call(data) as? SManga - } catch (_: Exception) { null } + } catch (_: Exception) { + null + } val genres = try { (data::class.members.find { it.name == "genres" }?.call(data) as? List<*>)?.mapNotNull { it.toString() } ?: emptyList() - } catch (_: Exception) { emptyList() } + } catch (_: Exception) { + emptyList() + } val altTitles = try { (data::class.members.find { it.name == "altTitles" }?.call(data) as? List<*>)?.mapNotNull { it.toString() } ?: emptyList() - } catch (_: Exception) { emptyList() } + } catch (_: Exception) { + emptyList() + } val statusString = try { data::class.members.find { it.name == "statusString" }?.call(data) as? String ?: "" - } catch (_: Exception) { "" } + } catch (_: Exception) { + "" + } if (smanga == null) return@filter false @@ -146,7 +154,9 @@ abstract class MangaCloud : HttpSource() { val mangas = filtered.mapNotNull { data -> try { data::class.members.find { it.name == "smanga" }?.call(data) as? SManga - } catch (_: Exception) { null } + } catch (_: Exception) { + null + } } return MangasPage(mangas, false) From cfaa2bd12f7aac47a619597e5fbd74785fc0df03 Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 16:20:41 +0300 Subject: [PATCH 19/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 48 ++++--------------- 1 file changed, 9 insertions(+), 39 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index ac184eb9007..b204eb04c23 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud +import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -76,7 +77,7 @@ abstract class MangaCloud : HttpSource() { val genre = currentGenreFilter val status = currentStatusFilter - val allItems = mutableListOf() + val allItems = mutableListOf() var nextPageToken: String? = null var currentResponse = response @@ -106,60 +107,29 @@ abstract class MangaCloud : HttpSource() { } while (nextPageToken != null && currentResponse.isSuccessful) val filtered = allItems.filter { data -> - val smanga = try { - data::class.members.find { it.name == "smanga" }?.call(data) as? SManga - } catch (_: Exception) { - null - } - val genres = try { - (data::class.members.find { it.name == "genres" }?.call(data) as? List<*>)?.mapNotNull { it.toString() } ?: emptyList() - } catch (_: Exception) { - emptyList() - } - val altTitles = try { - (data::class.members.find { it.name == "altTitles" }?.call(data) as? List<*>)?.mapNotNull { it.toString() } ?: emptyList() - } catch (_: Exception) { - emptyList() - } - val statusString = try { - data::class.members.find { it.name == "statusString" }?.call(data) as? String ?: "" - } catch (_: Exception) { - "" - } - - if (smanga == null) return@filter false - val matchesQuery = if (query.isNotBlank()) { val q = query.lowercase() - smanga.title.lowercase().contains(q) || - altTitles.any { it.lowercase().contains(q) } || - genres.any { it.lowercase().contains(q) } || - smanga.description?.lowercase()?.contains(q) == true + data.smanga.title.lowercase().contains(q) || + data.altTitles.any { it.lowercase().contains(q) } || + data.genres.any { it.lowercase().contains(q) } || + data.smanga.description?.lowercase()?.contains(q) == true } else { true } val matchesGenre = if (genre.isNotBlank()) { - genres.any { it.lowercase().contains(genre.lowercase()) } + data.genres.any { it.lowercase().contains(genre.lowercase()) } } else { true } val matchesStatus = if (status.isNotBlank()) { - statusString == status + data.statusString == status } else { true } matchesQuery && matchesGenre && matchesStatus } - val mangas = filtered.mapNotNull { data -> - try { - data::class.members.find { it.name == "smanga" }?.call(data) as? SManga - } catch (_: Exception) { - null - } - } - - return MangasPage(mangas, false) + return MangasPage(filtered.map { it.smanga }, false) } override fun mangaDetailsRequest(manga: SManga): Request { From 5f24b0cf6b1310828cec697b9aed67b4e726f2af Mon Sep 17 00:00:00 2001 From: hanihamed1998 Date: Fri, 10 Jul 2026 16:25:53 +0300 Subject: [PATCH 20/24] Update MangaCloud.kt --- .../extension/ar/mangacloud/MangaCloud.kt | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index b204eb04c23..1d1c0b1af20 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,6 +1,5 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud -import eu.kanade.tachiyomi.extension.ar.mangacloud.FirestoreParser import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -36,6 +35,9 @@ abstract class MangaCloud : HttpSource() { override fun popularMangaParse(response: Response): MangasPage { val json = FirestoreParser.parseList(response) lastPageToken = json.nextPageToken + if (allMangasCache.isEmpty()) { + allMangasCache.addAll(json.mangas) + } return MangasPage(json.mangas.map { it.smanga }, json.nextPageToken != null) } @@ -58,7 +60,6 @@ abstract class MangaCloud : HttpSource() { currentSortFilter = filters.firstInstanceOrNull()?.selected.orEmpty() currentGenreFilter = filters.firstInstanceOrNull()?.selected.orEmpty() currentStatusFilter = filters.firstInstanceOrNull()?.selected.orEmpty() - val url = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", "300") .addQueryParameter("key", API_KEY) @@ -76,37 +77,28 @@ abstract class MangaCloud : HttpSource() { val query = currentSearchQuery.trim() val genre = currentGenreFilter val status = currentStatusFilter + val json = FirestoreParser.parseList(response) + val allMangas = json.mangas.toMutableList() - val allItems = mutableListOf() - var nextPageToken: String? = null - var currentResponse = response + if (allMangasCache.isEmpty()) { + allMangasCache.addAll(json.mangas) + } - do { - val jsonString = currentResponse.body?.string() ?: break - val newResponse = currentResponse.newBuilder() - .body(jsonString.toResponseBody(null)) + var nextPageToken = json.nextPageToken + while (nextPageToken != null) { + val url = FIRESTORE_URL.toHttpUrl().newBuilder() + .addQueryParameter("pageSize", "300") + .addQueryParameter("key", API_KEY) + .addQueryParameter("pageToken", nextPageToken) .build() - val parsed = FirestoreParser.parseList(newResponse) - allItems.addAll(parsed.mangas) - nextPageToken = parsed.nextPageToken - - if (nextPageToken != null) { - val urlBuilder = FIRESTORE_URL.toHttpUrl().newBuilder() - .addQueryParameter("pageSize", "300") - .addQueryParameter("key", API_KEY) - .addQueryParameter("pageToken", nextPageToken) - .apply { - when (currentSortFilter) { - "new" -> addQueryParameter("orderBy", "createdAt desc") - "updated" -> addQueryParameter("orderBy", "updatedAt desc") - } - } - val newRequest = GET(urlBuilder.build(), headers) - currentResponse = client.newCall(newRequest).execute() - } - } while (nextPageToken != null && currentResponse.isSuccessful) + val nextResponse = client.newCall(GET(url, headers)).execute() + val nextJson = FirestoreParser.parseList(nextResponse) + allMangas.addAll(nextJson.mangas) + allMangasCache.addAll(nextJson.mangas) + nextPageToken = nextJson.nextPageToken + } - val filtered = allItems.filter { data -> + val filtered = allMangas.filter { data -> val matchesQuery = if (query.isNotBlank()) { val q = query.lowercase() data.smanga.title.lowercase().contains(q) || @@ -128,7 +120,6 @@ abstract class MangaCloud : HttpSource() { } matchesQuery && matchesGenre && matchesStatus } - return MangasPage(filtered.map { it.smanga }, false) } @@ -220,6 +211,7 @@ abstract class MangaCloud : HttpSource() { private var currentSortFilter: String = "" private var currentGenreFilter: String = "" private var currentStatusFilter: String = "" + private val allMangasCache = mutableListOf() companion object { private const val API_KEY = "AIzaSyAvdmgz_r_d89Eo8JBs9vAjUAJR451bMYU" @@ -240,6 +232,7 @@ abstract class MangaCloud : HttpSource() { private val genreList = arrayOf( Pair("الكل", ""), + // التصنيفات الأصلية + الجديدة (مرتبة أبجدياً، بدون تكرار) Pair("إدارة المناطق", "إدارة المناطق"), Pair("إعادة احياء", "إعادة احياء"), Pair("إعادة بحث", "إعادة بحث"), From b0c40ab51f096af270b3d3836bcf013698cd852e Mon Sep 17 00:00:00 2001 From: yemen Date: Sat, 11 Jul 2026 21:14:48 +0300 Subject: [PATCH 21/24] fix(ar/mangacloud): add missing ContentWarning import --- src/ar/mangacloud/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ar/mangacloud/build.gradle.kts b/src/ar/mangacloud/build.gradle.kts index fb78d82d6c6..48441ad6a40 100644 --- a/src/ar/mangacloud/build.gradle.kts +++ b/src/ar/mangacloud/build.gradle.kts @@ -1,3 +1,5 @@ +import io.github.keiyoushi.gradle.api.ContentWarning + plugins { alias(kei.plugins.extension) } From 6410fc8ab5e1f3748aec86d10f98cf580e399568 Mon Sep 17 00:00:00 2001 From: yemen Date: Wed, 15 Jul 2026 16:40:18 +0300 Subject: [PATCH 22/24] fix(ar/mangacloud): address reviewer feedback - use kotlinx.serialization, save only id, move filters to separate file --- .../tachiyomi/extension/ar/mangacloud/Dto.kt | 67 +++-- .../extension/ar/mangacloud/Filters.kt | 250 ++++++++++++++++ .../extension/ar/mangacloud/MangaCloud.kt | 271 +----------------- 3 files changed, 296 insertions(+), 292 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt index 116379f6c2d..13af9303418 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt @@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import keiyoushi.utils.jsonInstance +import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonArray @@ -11,6 +12,18 @@ import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import okhttp3.Response +@Serializable +data class FirestoreListResponse( + val documents: List? = null, + val nextPageToken: String? = null, +) + +@Serializable +data class FirestoreDocument( + val name: String? = null, + val fields: JsonObject? = null, +) + data class MangaData( val smanga: SManga, val altTitles: List = emptyList(), @@ -26,24 +39,20 @@ data class MangaListResult( object FirestoreParser { fun parseList(response: Response): MangaListResult { - val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject - val documents = json["documents"]?.jsonArray ?: return MangaListResult(emptyList(), null) - val mangas = documents.mapNotNull { doc -> - val fields = doc.jsonObject["fields"]?.jsonObject ?: return@mapNotNull null - val mangaId = doc.jsonObject["name"]?.jsonPrimitive?.contentOrNull?.substringAfterLast("/") ?: return@mapNotNull null - val altTitles = fields.stringArray("alternativeTitles") - val genres = fields.stringArray("genres") - val statusStr = fields.string("status") + val result = jsonInstance.decodeFromString(response.body.string()) + val mangas = result.documents?.mapNotNull { doc -> + val fields = doc.fields ?: return@mapNotNull null + val mangaId = doc.name?.substringAfterLast("/") ?: return@mapNotNull null MangaData( smanga = SManga.create().apply { - url = "/manga/$mangaId" + url = mangaId title = fields.string("title") thumbnail_url = fields.string("coverImage") description = fields.string("description") author = fields.string("author") artist = fields.string("artist") - genre = genres.joinToString(", ") - status = when (statusStr) { + genre = fields.stringArray("genres").joinToString(", ") + status = when (fields.string("status")) { "مستمر" -> SManga.ONGOING "مكتمل" -> SManga.COMPLETED "متروك", "متروكة" -> SManga.ON_HIATUS @@ -51,25 +60,24 @@ object FirestoreParser { else -> SManga.UNKNOWN } }, - altTitles = altTitles, - genres = genres, - statusString = when (statusStr) { + altTitles = fields.stringArray("alternativeTitles"), + genres = fields.stringArray("genres"), + statusString = when (fields.string("status")) { "مستمر" -> "ongoing" "مكتمل" -> "completed" else -> "" }, ) - } - val nextPageToken = json["nextPageToken"]?.jsonPrimitive?.contentOrNull - return MangaListResult(mangas, nextPageToken) + } ?: emptyList() + return MangaListResult(mangas, result.nextPageToken) } fun parseDetails(response: Response): SManga { - val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject - val fields = json["fields"]?.jsonObject ?: return SManga.create() - val mangaId = json["name"]?.jsonPrimitive?.contentOrNull?.substringAfterLast("/") ?: "" + val doc = jsonInstance.decodeFromString(response.body.string()) + val fields = doc.fields ?: return SManga.create() + val mangaId = doc.name?.substringAfterLast("/") ?: "" return SManga.create().apply { - url = "/manga/$mangaId" + url = mangaId title = fields.string("title") thumbnail_url = fields.string("coverImage") description = fields.string("description") @@ -86,11 +94,9 @@ object FirestoreParser { } fun parseChapters(response: Response): List { - val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject - val documents = json["documents"]?.jsonArray ?: return emptyList() - return documents.mapNotNull { doc -> - val fields = doc.jsonObject["fields"]?.jsonObject ?: return@mapNotNull null - val docName = doc.jsonObject["name"]?.jsonPrimitive?.contentOrNull ?: return@mapNotNull null + val result = jsonInstance.decodeFromString(response.body.string()) + return result.documents?.mapNotNull { doc -> + val docName = doc.name ?: return@mapNotNull null val chapterNum = docName.substringAfterLast("/") val num = chapterNum.toIntOrNull() ?: 0 SChapter.create().apply { @@ -98,13 +104,14 @@ object FirestoreParser { name = "الفصل $num" chapter_number = num.toFloat() } - }.sortedByDescending { it.chapter_number } + }?.sortedByDescending { it.chapter_number } ?: emptyList() } fun parsePages(response: Response): List { - val json = jsonInstance.parseToJsonElement(response.body.string()).jsonObject - val fields = json["fields"]?.jsonObject ?: return emptyList() - val pagesArray = fields["pages"]?.jsonObject?.get("arrayValue")?.jsonObject?.get("values")?.jsonArray ?: return emptyList() + val doc = jsonInstance.decodeFromString(response.body.string()) + val fields = doc.fields ?: return emptyList() + val pagesArray = fields["pages"]?.jsonObject?.get("arrayValue")?.jsonObject + ?.get("values")?.jsonArray ?: return emptyList() return pagesArray.mapIndexed { index, value -> Page(index, imageUrl = value.jsonObject["stringValue"]?.jsonPrimitive?.contentOrNull ?: "") } diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt index c73360fce80..11d5fc9922a 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Filters.kt @@ -13,3 +13,253 @@ internal class GenreFilter(name: String, private val pairs: Array>) : Filter.Select(name, pairs.map { it.first }.toTypedArray()) { val selected get() = pairs[state].second } + +internal val sortingList = arrayOf( + Pair("الأشهر", "popular"), + Pair("آخر التحديثات", "updated"), + Pair("إضافات جديدة", "new"), +) + +internal val statusList = arrayOf( + Pair("الكل", ""), + Pair("مستمر", "ongoing"), + Pair("مكتمل", "completed"), +) + +internal val genreList = arrayOf( + Pair("الكل", ""), + Pair("إدارة المناطق", "إدارة المناطق"), + Pair("إعادة احياء", "إعادة احياء"), + Pair("إعادة بحث", "إعادة بحث"), + Pair("إنتقام", "إنتقام"), + Pair("إيسيكاي", "إيسيكاي"), + Pair("ابراج", "ابراج"), + Pair("اثاره", "اثاره"), + Pair("اتصال بالعالم الخارجي", "اتصال بالعالم الخارجي"), + Pair("اجناس", "اجناس"), + Pair("احتجاج", "احتجاج"), + Pair("اختفاء", "اختفاء"), + Pair("ارتقاء", "ارتقاء"), + Pair("ارواح", "ارواح"), + Pair("ازياء", "ازياء"), + Pair("اساطير", "اساطير"), + Pair("اساطيز", "اساطيز"), + Pair("اسبوعى", "اسبوعى"), + Pair("استراتيجية", "استراتيجية"), + Pair("اشباح", "اشباح"), + Pair("اضطهاد", "اضطهاد"), + Pair("اظطهاد", "اظطهاد"), + Pair("اعادة احياء", "اعادة احياء"), + Pair("اعمار", "اعمار"), + Pair("اعمال", "اعمال"), + Pair("اقتصاد", "اقتصاد"), + Pair("اكاديميه", "اكاديميه"), + Pair("اكشن", "اكشن"), + Pair("الات", "الات"), + Pair("الالوان الممتلئه", "الالوان الممتلئه"), + Pair("البقاء علي قيد الحياه", "البقاء علي قيد الحياه"), + Pair("الجانب المظلم من الحياه", "الجانب المظلم من الحياه"), + Pair("الحريم العكسي", "الحريم العكسي"), + Pair("الحياة المدرسيه", "الحياة المدرسيه"), + Pair("الحياة اليومية", "الحياة اليومية"), + Pair("الحيوانات الأليفة", "الحيوانات الأليفة"), + Pair("الخيال العلمي", "الخيال العلمي"), + Pair("السفر عبر الزمن", "السفر عبر الزمن"), + Pair("العاب", "العاب"), + Pair("العاب الكترونية", "العاب الكترونية"), + Pair("العاب تقليدية", "العاب تقليدية"), + Pair("العاب رعب", "العاب رعب"), + Pair("العاب فيديو", "العاب فيديو"), + Pair("العصور الوسطى", "العصور الوسطى"), + Pair("الغموض", "الغموض"), + Pair("الفتاة الوحش", "الفتاة الوحش"), + Pair("الفنون القتالية", "الفنون القتالية"), + Pair("الفنون العسكرية", "الفنون العسكرية"), + Pair("المخالفون للقانون", "المخالفون للقانون"), + Pair("النجاة", "النجاة"), + Pair("الهة", "الهة"), + Pair("الهه", "الهه"), + Pair("الواقع الافتراضي", "الواقع الافتراضي"), + Pair("اليات", "اليات"), + Pair("امرأة شريرة", "امرأة شريرة"), + Pair("اميرة", "اميرة"), + Pair("انتقال", "انتقال"), + Pair("انمى", "انمى"), + Pair("ايتشى", "ايتشى"), + Pair("ايسكاى", "ايسكاى"), + Pair("ايشي", "ايشي"), + Pair("بالغ", "بالغ"), + Pair("بزنس", "بزنس"), + Pair("بطل خارق", "بطل خارق"), + Pair("بطل غير اعتيادي", "بطل غير اعتيادي"), + Pair("بطل غير اعتيادى", "بطل غير اعتيادى"), + Pair("بطل مجنون", "بطل مجنون"), + Pair("بطل وحش", "بطل وحش"), + Pair("بعد الكارثه", "بعد الكارثه"), + Pair("بوليسي", "بوليسي"), + Pair("تاريخ", "تاريخ"), + Pair("تاريخى", "تاريخى"), + Pair("تجاره", "تجاره"), + Pair("تجسيد", "تجسيد"), + Pair("تحديث", "تحديث"), + Pair("تحري", "تحري"), + Pair("تحقيق", "تحقيق"), + Pair("تحقيقات", "تحقيقات"), + Pair("تخطيط", "تخطيط"), + Pair("تدريب", "تدريب"), + Pair("تراجع", "تراجع"), + Pair("تراجيدي", "تراجيدي"), + Pair("ترويض", "ترويض"), + Pair("ترويض وحوش", "ترويض وحوش"), + Pair("تشويق", "تشويق"), + Pair("تلوين رسم", "تلوين رسم"), + Pair("تلوين رسمي", "تلوين رسمي"), + Pair("تلوين هواة", "تلوين هواة"), + Pair("تملك", "تملك"), + Pair("تناسخ", "تناسخ"), + Pair("تناسخ الارواح", "تناسخ الارواح"), + Pair("تنانين", "تنانين"), + Pair("تنايخ", "تنايخ"), + Pair("ثأر", "ثأر"), + Pair("جانحون", "جانحون"), + Pair("جريمة", "جريمة"), + Pair("جريمه", "جريمه"), + Pair("جندر اسواب", "جندر اسواب"), + Pair("جندر بندير", "جندر بندير"), + Pair("جوسي", "جوسي"), + Pair("جوسين", "جوسين"), + Pair("جوسيه", "جوسيه"), + Pair("حائز علي جائزة", "حائز علي جائزة"), + Pair("حديث", "حديث"), + Pair("حرب", "حرب"), + Pair("حربي", "حربي"), + Pair("حريم", "حريم"), + Pair("حريم عكسي", "حريم عكسي"), + Pair("حصرية", "حصرية"), + Pair("حياة مدرسية", "حياة مدرسية"), + Pair("حياة يومية", "حياة يومية"), + Pair("حيوانات", "حيوانات"), + Pair("حيوانات اليفه", "حيوانات اليفه"), + Pair("خارق", "خارق"), + Pair("خارق للطبيعه", "خارق للطبيعه"), + Pair("خيار", "خيار"), + Pair("خيال", "خيال"), + Pair("خيال علمي", "خيال علمي"), + Pair("خيالي", "خيالي"), + Pair("داخل اللعبه", "داخل اللعبه"), + Pair("داخل روايه", "داخل روايه"), + Pair("دراما", "دراما"), + Pair("دماء", "دماء"), + Pair("دموى", "دموى"), + Pair("ذكريات من عالم آخر", "ذكريات من عالم آخر"), + Pair("راشد", "راشد"), + Pair("رعاية اطفال", "رعاية اطفال"), + Pair("رعب", "رعب"), + Pair("روايات عربية", "روايات عربية"), + Pair("روايه", "روايه"), + Pair("رومانسي", "رومانسي"), + Pair("رياضه", "رياضه"), + Pair("رياضي", "رياضي"), + Pair("زراعة", "زراعة"), + Pair("زمكاني", "زمكاني"), + Pair("زمنكاني", "زمنكاني"), + Pair("زنزانات", "زنزانات"), + Pair("زواج مدبر", "زواج مدبر"), + Pair("زومبي", "زومبي"), + Pair("ساموراي", "ساموراي"), + Pair("ساموري", "ساموري"), + Pair("سايكوباث", "سايكوباث"), + Pair("سحر", "سحر"), + Pair("سيرة ذاتية", "سيرة ذاتية"), + Pair("سياسي", "سياسي"), + Pair("سينين", "سينين"), + Pair("شرطة", "شرطة"), + Pair("شركه", "شركه"), + Pair("شريحة من الحياة", "شريحة من الحياة"), + Pair("شرير", "شرير"), + Pair("شوجو", "شوجو"), + Pair("شونين", "شونين"), + Pair("شياطين", "شياطين"), + Pair("شينين", "شينين"), + Pair("صقل", "صقل"), + Pair("طبخ", "طبخ"), + Pair("طبي", "طبي"), + Pair("طرد الارواح الشريره", "طرد الارواح الشريره"), + Pair("عائلي", "عائلي"), + Pair("عالم مختلف", "عالم مختلف"), + Pair("عامل مكتبي", "عامل مكتبي"), + Pair("عسكري", "عسكري"), + Pair("عسكريه", "عسكريه"), + Pair("عصر حديث", "عصر حديث"), + Pair("عصور وسطى", "عصور وسطى"), + Pair("علم نفس", "علم نفس"), + Pair("علمي", "علمي"), + Pair("عنف", "عنف"), + Pair("عنف جنسي", "عنف جنسي"), + Pair("عوالم", "عوالم"), + Pair("عوده بالزمن", "عوده بالزمن"), + Pair("غموض", "غموض"), + Pair("فانتازا", "فانتازا"), + Pair("فانتازيا", "فانتازيا"), + Pair("فانتسي", "فانتسي"), + Pair("فلسفه", "فلسفه"), + Pair("فنتازيا", "فنتازيا"), + Pair("فنون الدفاع عن النفس", "فنون الدفاع عن النفس"), + Pair("فنون قتاليه", "فنون قتاليه"), + Pair("فوق الطبيعه", "فوق الطبيعه"), + Pair("فيكتوري", "فيكتوري"), + Pair("قتال", "قتال"), + Pair("قتالات", "قتالات"), + Pair("قوة خارقة", "قوة خارقة"), + Pair("كائنات فضائية", "كائنات فضائية"), + Pair("كل الاعمار", "كل الاعمار"), + Pair("كوميديا", "كوميديا"), + Pair("كوميدى", "كوميدى"), + Pair("كوميك", "كوميك"), + Pair("كوما-4", "كوما-4"), + Pair("لعبه", "لعبه"), + Pair("مأساوي", "مأساوي"), + Pair("مؤامرات", "مؤامرات"), + Pair("ما بعد نهاية العالم", "ما بعد نهاية العالم"), + Pair("مأساة", "مأساة"), + Pair("مافيا", "مافيا"), + Pair("مانا", "مانا"), + Pair("مانجا", "مانجا"), + Pair("مانجا ملونه", "مانجا ملونه"), + Pair("مانها", "مانها"), + Pair("مانهوا", "مانهوا"), + Pair("متحول", "متحول"), + Pair("مجموعة قصص", "مجموعة قصص"), + Pair("مدرسه", "مدرسه"), + Pair("مدرسي", "مدرسي"), + Pair("مستذئب", "مستذئب"), + Pair("مصاصي الدماء", "مصاصي الدماء"), + Pair("معالج", "معالج"), + Pair("مغامرة", "مغامرة"), + Pair("مغني", "مغني"), + Pair("مقتبسة", "مقتبسة"), + Pair("مقطع طولي", "مقطع طولي"), + Pair("ملائكة", "ملائكة"), + Pair("ملونه", "ملونه"), + Pair("ممالك", "ممالك"), + Pair("موريم", "موريم"), + Pair("موسيقى", "موسيقى"), + Pair("ميكا", "ميكا"), + Pair("ناضج", "ناضج"), + Pair("نبالة", "نبالة"), + Pair("نبلاء", "نبلاء"), + Pair("نظام", "نظام"), + Pair("نفسي", "نفسي"), + Pair("نهاية العالم", "نهاية العالم"), + Pair("نينجا", "نينجا"), + Pair("هندسة", "هندسة"), + Pair("هواه", "هواه"), + Pair("هوس", "هوس"), + Pair("واقع افتراضي", "واقع افتراضي"), + Pair("واقعى", "واقعى"), + Pair("ويبتون", "ويبتون"), + Pair("وحوش", "وحوش"), + Pair("ون شوت", "ون شوت"), + Pair("ويب تون", "ويب تون"), + Pair("يقاء", "يقاء"), +) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 1d1c0b1af20..1b9d94ace6a 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -9,11 +9,14 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import keiyoushi.annotation.Source import keiyoushi.utils.firstInstanceOrNull +import keiyoushi.utils.jsonInstance +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.Request import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody -import org.json.JSONObject @Source abstract class MangaCloud : HttpSource() { @@ -124,16 +127,14 @@ abstract class MangaCloud : HttpSource() { } override fun mangaDetailsRequest(manga: SManga): Request { - val mangaId = manga.url.substringAfterLast("/") - val url = "$FIRESTORE_URL/$mangaId?key=$API_KEY".toHttpUrl() + val url = "$FIRESTORE_URL/${manga.url}?key=$API_KEY".toHttpUrl() return GET(url, headers) } override fun mangaDetailsParse(response: Response): SManga = FirestoreParser.parseDetails(response) override fun chapterListRequest(manga: SManga): Request { - val mangaId = manga.url.substringAfterLast("/") - val url = "$FIRESTORE_URL/$mangaId/chapters?pageSize=1000&key=$API_KEY&orderBy=index".toHttpUrl() + val url = "$FIRESTORE_URL/${manga.url}/chapters?pageSize=1000&key=$API_KEY&orderBy=index".toHttpUrl() return GET(url, headers) } @@ -144,7 +145,7 @@ abstract class MangaCloud : HttpSource() { do { val jsonString = currentResponse.body?.string() ?: break - val json = JSONObject(jsonString) + val json = jsonInstance.parseToJsonElement(jsonString).jsonObject val newResponse = currentResponse.newBuilder() .body(jsonString.toResponseBody(null)) @@ -152,7 +153,7 @@ abstract class MangaCloud : HttpSource() { val chapters = FirestoreParser.parseChapters(newResponse) allChapters.addAll(chapters) - nextPageToken = json.optString("nextPageToken").takeIf { it.isNotEmpty() } + nextPageToken = json["nextPageToken"]?.jsonPrimitive?.contentOrNull if (nextPageToken != null) { val pathSegments = currentResponse.request.url.pathSegments @@ -187,10 +188,7 @@ abstract class MangaCloud : HttpSource() { override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() - override fun getMangaUrl(manga: SManga): String { - val mangaId = manga.url.substringAfterLast("/") - return "$baseUrl/manga/$mangaId" - } + override fun getMangaUrl(manga: SManga): String = "$baseUrl/manga/${manga.url}" override fun getChapterUrl(chapter: SChapter): String { val parts = chapter.url.split("/") @@ -217,256 +215,5 @@ abstract class MangaCloud : HttpSource() { private const val API_KEY = "AIzaSyAvdmgz_r_d89Eo8JBs9vAjUAJR451bMYU" private const val FIRESTORE_URL = "https://firestore.googleapis.com/v1/projects/ninja-brew-crew-4d79c/databases/(default)/documents/cloudmangas" private const val PAGE_SIZE = 20 - - private val sortingList = arrayOf( - Pair("الأشهر", "popular"), - Pair("آخر التحديثات", "updated"), - Pair("إضافات جديدة", "new"), - ) - - private val statusList = arrayOf( - Pair("الكل", ""), - Pair("مستمر", "ongoing"), - Pair("مكتمل", "completed"), - ) - - private val genreList = arrayOf( - Pair("الكل", ""), - // التصنيفات الأصلية + الجديدة (مرتبة أبجدياً، بدون تكرار) - Pair("إدارة المناطق", "إدارة المناطق"), - Pair("إعادة احياء", "إعادة احياء"), - Pair("إعادة بحث", "إعادة بحث"), - Pair("إنتقام", "إنتقام"), - Pair("إيسيكاي", "إيسيكاي"), - Pair("ابراج", "ابراج"), - Pair("اثاره", "اثاره"), - Pair("اتصال بالعالم الخارجي", "اتصال بالعالم الخارجي"), - Pair("اجناس", "اجناس"), - Pair("احتجاج", "احتجاج"), - Pair("اختفاء", "اختفاء"), - Pair("ارتقاء", "ارتقاء"), - Pair("ارواح", "ارواح"), - Pair("ازياء", "ازياء"), - Pair("اساطير", "اساطير"), - Pair("اساطيز", "اساطيز"), - Pair("اسبوعى", "اسبوعى"), - Pair("استراتيجية", "استراتيجية"), - Pair("اشباح", "اشباح"), - Pair("اضطهاد", "اضطهاد"), - Pair("اظطهاد", "اظطهاد"), - Pair("اعادة احياء", "اعادة احياء"), - Pair("اعمار", "اعمار"), - Pair("اعمال", "اعمال"), - Pair("اقتصاد", "اقتصاد"), - Pair("اكاديميه", "اكاديميه"), - Pair("اكشن", "اكشن"), - Pair("الات", "الات"), - Pair("الالوان الممتلئه", "الالوان الممتلئه"), - Pair("البقاء علي قيد الحياه", "البقاء علي قيد الحياه"), - Pair("الجانب المظلم من الحياه", "الجانب المظلم من الحياه"), - Pair("الحريم العكسي", "الحريم العكسي"), - Pair("الحياة المدرسيه", "الحياة المدرسيه"), - Pair("الحياة اليومية", "الحياة اليومية"), - Pair("الحيوانات الأليفة", "الحيوانات الأليفة"), - Pair("الخيال العلمي", "الخيال العلمي"), - Pair("السفر عبر الزمن", "السفر عبر الزمن"), - Pair("العاب", "العاب"), - Pair("العاب الكترونية", "العاب الكترونية"), - Pair("العاب تقليدية", "العاب تقليدية"), - Pair("العاب رعب", "العاب رعب"), - Pair("العاب فيديو", "العاب فيديو"), - Pair("العصور الوسطى", "العصور الوسطى"), - Pair("الغموض", "الغموض"), - Pair("الفتاة الوحش", "الفتاة الوحش"), - Pair("الفنون القتالية", "الفنون القتالية"), - Pair("الفنون العسكرية", "الفنون العسكرية"), - Pair("المخالفون للقانون", "المخالفون للقانون"), - Pair("النجاة", "النجاة"), - Pair("الهة", "الهة"), - Pair("الهه", "الهه"), - Pair("الواقع الافتراضي", "الواقع الافتراضي"), - Pair("اليات", "اليات"), - Pair("امرأة شريرة", "امرأة شريرة"), - Pair("اميرة", "اميرة"), - Pair("انتقال", "انتقال"), - Pair("انمى", "انمى"), - Pair("ايتشى", "ايتشى"), - Pair("ايسكاى", "ايسكاى"), - Pair("ايشي", "ايشي"), - Pair("بالغ", "بالغ"), - Pair("بزنس", "بزنس"), - Pair("بطل خارق", "بطل خارق"), - Pair("بطل غير اعتيادي", "بطل غير اعتيادي"), - Pair("بطل غير اعتيادى", "بطل غير اعتيادى"), - Pair("بطل مجنون", "بطل مجنون"), - Pair("بطل وحش", "بطل وحش"), - Pair("بعد الكارثه", "بعد الكارثه"), - Pair("بوليسي", "بوليسي"), - Pair("تاريخ", "تاريخ"), - Pair("تاريخى", "تاريخى"), - Pair("تجاره", "تجاره"), - Pair("تجسيد", "تجسيد"), - Pair("تحديث", "تحديث"), - Pair("تحري", "تحري"), - Pair("تحقيق", "تحقيق"), - Pair("تحقيقات", "تحقيقات"), - Pair("تخطيط", "تخطيط"), - Pair("تدريب", "تدريب"), - Pair("تراجع", "تراجع"), - Pair("تراجيدي", "تراجيدي"), - Pair("ترويض", "ترويض"), - Pair("ترويض وحوش", "ترويض وحوش"), - Pair("تشويق", "تشويق"), - Pair("تلوين رسم", "تلوين رسم"), - Pair("تلوين رسمي", "تلوين رسمي"), - Pair("تلوين هواة", "تلوين هواة"), - Pair("تملك", "تملك"), - Pair("تناسخ", "تناسخ"), - Pair("تناسخ الارواح", "تناسخ الارواح"), - Pair("تنانين", "تنانين"), - Pair("تنايخ", "تنايخ"), - Pair("ثأر", "ثأر"), - Pair("جانحون", "جانحون"), - Pair("جريمة", "جريمة"), - Pair("جريمه", "جريمه"), - Pair("جندر اسواب", "جندر اسواب"), - Pair("جندر بندير", "جندر بندير"), - Pair("جوسي", "جوسي"), - Pair("جوسين", "جوسين"), - Pair("جوسيه", "جوسيه"), - Pair("حائز علي جائزة", "حائز علي جائزة"), - Pair("حديث", "حديث"), - Pair("حرب", "حرب"), - Pair("حربي", "حربي"), - Pair("حريم", "حريم"), - Pair("حريم عكسي", "حريم عكسي"), - Pair("حصرية", "حصرية"), - Pair("حياة مدرسية", "حياة مدرسية"), - Pair("حياة يومية", "حياة يومية"), - Pair("حيوانات", "حيوانات"), - Pair("حيوانات اليفه", "حيوانات اليفه"), - Pair("خارق", "خارق"), - Pair("خارق للطبيعه", "خارق للطبيعه"), - Pair("خيار", "خيار"), - Pair("خيال", "خيال"), - Pair("خيال علمي", "خيال علمي"), - Pair("خيالي", "خيالي"), - Pair("داخل اللعبه", "داخل اللعبه"), - Pair("داخل روايه", "داخل روايه"), - Pair("دراما", "دراما"), - Pair("دماء", "دماء"), - Pair("دموى", "دموى"), - Pair("ذكريات من عالم آخر", "ذكريات من عالم آخر"), - Pair("راشد", "راشد"), - Pair("رعاية اطفال", "رعاية اطفال"), - Pair("رعب", "رعب"), - Pair("روايات عربية", "روايات عربية"), - Pair("روايه", "روايه"), - Pair("رومانسي", "رومانسي"), - Pair("رياضه", "رياضه"), - Pair("رياضي", "رياضي"), - Pair("زراعة", "زراعة"), - Pair("زمكاني", "زمكاني"), - Pair("زمنكاني", "زمنكاني"), - Pair("زنزانات", "زنزانات"), - Pair("زواج مدبر", "زواج مدبر"), - Pair("زومبي", "زومبي"), - Pair("ساموراي", "ساموراي"), - Pair("ساموري", "ساموري"), - Pair("سايكوباث", "سايكوباث"), - Pair("سحر", "سحر"), - Pair("سيرة ذاتية", "سيرة ذاتية"), - Pair("سياسي", "سياسي"), - Pair("سينين", "سينين"), - Pair("شرطة", "شرطة"), - Pair("شركه", "شركه"), - Pair("شريحة من الحياة", "شريحة من الحياة"), - Pair("شرير", "شرير"), - Pair("شوجو", "شوجو"), - Pair("شونين", "شونين"), - Pair("شياطين", "شياطين"), - Pair("شينين", "شينين"), - Pair("صقل", "صقل"), - Pair("طبخ", "طبخ"), - Pair("طبي", "طبي"), - Pair("طرد الارواح الشريره", "طرد الارواح الشريره"), - Pair("عائلي", "عائلي"), - Pair("عالم مختلف", "عالم مختلف"), - Pair("عامل مكتبي", "عامل مكتبي"), - Pair("عسكري", "عسكري"), - Pair("عسكريه", "عسكريه"), - Pair("عصر حديث", "عصر حديث"), - Pair("عصور وسطى", "عصور وسطى"), - Pair("علم نفس", "علم نفس"), - Pair("علمي", "علمي"), - Pair("عنف", "عنف"), - Pair("عنف جنسي", "عنف جنسي"), - Pair("عوالم", "عوالم"), - Pair("عوده بالزمن", "عوده بالزمن"), - Pair("غموض", "غموض"), - Pair("فانتازا", "فانتازا"), - Pair("فانتازيا", "فانتازيا"), - Pair("فانتسي", "فانتسي"), - Pair("فلسفه", "فلسفه"), - Pair("فنتازيا", "فنتازيا"), - Pair("فنون الدفاع عن النفس", "فنون الدفاع عن النفس"), - Pair("فنون قتاليه", "فنون قتاليه"), - Pair("فوق الطبيعه", "فوق الطبيعه"), - Pair("فيكتوري", "فيكتوري"), - Pair("قتال", "قتال"), - Pair("قتالات", "قتالات"), - Pair("قوة خارقة", "قوة خارقة"), - Pair("كائنات فضائية", "كائنات فضائية"), - Pair("كل الاعمار", "كل الاعمار"), - Pair("كوميديا", "كوميديا"), - Pair("كوميدى", "كوميدى"), - Pair("كوميك", "كوميك"), - Pair("كوما-4", "كوما-4"), - Pair("لعبه", "لعبه"), - Pair("مأساوي", "مأساوي"), - Pair("مؤامرات", "مؤامرات"), - Pair("ما بعد نهاية العالم", "ما بعد نهاية العالم"), - Pair("مأساة", "مأساة"), - Pair("مافيا", "مافيا"), - Pair("مانا", "مانا"), - Pair("مانجا", "مانجا"), - Pair("مانجا ملونه", "مانجا ملونه"), - Pair("مانها", "مانها"), - Pair("مانهوا", "مانهوا"), - Pair("متحول", "متحول"), - Pair("مجموعة قصص", "مجموعة قصص"), - Pair("مدرسه", "مدرسه"), - Pair("مدرسي", "مدرسي"), - Pair("مستذئب", "مستذئب"), - Pair("مصاصي الدماء", "مصاصي الدماء"), - Pair("معالج", "معالج"), - Pair("مغامرة", "مغامرة"), - Pair("مغني", "مغني"), - Pair("مقتبسة", "مقتبسة"), - Pair("مقطع طولي", "مقطع طولي"), - Pair("ملائكة", "ملائكة"), - Pair("ملونه", "ملونه"), - Pair("ممالك", "ممالك"), - Pair("موريم", "موريم"), - Pair("موسيقى", "موسيقى"), - Pair("ميكا", "ميكا"), - Pair("ناضج", "ناضج"), - Pair("نبالة", "نبالة"), - Pair("نبلاء", "نبلاء"), - Pair("نظام", "نظام"), - Pair("نفسي", "نفسي"), - Pair("نهاية العالم", "نهاية العالم"), - Pair("نينجا", "نينجا"), - Pair("هندسة", "هندسة"), - Pair("هواه", "هواه"), - Pair("هوس", "هوس"), - Pair("واقع افتراضي", "واقع افتراضي"), - Pair("واقعى", "واقعى"), - Pair("ويبتون", "ويبتون"), - Pair("وحوش", "وحوش"), - Pair("ون شوت", "ون شوت"), - Pair("ويب تون", "ويب تون"), - Pair("يقاء", "يقاء"), - ) } } From 5bea7727bbd076f4af3b9b7da9f35895427d1132 Mon Sep 17 00:00:00 2001 From: yemen Date: Thu, 16 Jul 2026 15:32:18 +0300 Subject: [PATCH 23/24] fix(ar/mangacloud): implement KeiSource, set libVersion to 1.6 --- src/ar/mangacloud/build.gradle.kts | 2 +- .../extension/ar/mangacloud/MangaCloud.kt | 162 ++++++++++-------- 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/src/ar/mangacloud/build.gradle.kts b/src/ar/mangacloud/build.gradle.kts index 48441ad6a40..4dc0e85d57e 100644 --- a/src/ar/mangacloud/build.gradle.kts +++ b/src/ar/mangacloud/build.gradle.kts @@ -8,7 +8,7 @@ keiyoushi { name = "MangaCloud" versionCode = 1 contentWarning = ContentWarning.SAFE - libVersion = "1.4" + libVersion = "1.6" source { lang = "ar" diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index 1b9d94ace6a..b60f72847df 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -1,29 +1,30 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud -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.HttpSource +import eu.kanade.tachiyomi.source.model.SMangaUpdate import keiyoushi.annotation.Source +import keiyoushi.network.get +import keiyoushi.source.KeiSource import keiyoushi.utils.firstInstanceOrNull import keiyoushi.utils.jsonInstance import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl -import okhttp3.Request -import okhttp3.Response -import okhttp3.ResponseBody.Companion.toResponseBody @Source -abstract class MangaCloud : HttpSource() { +abstract class MangaCloud : KeiSource() { override val supportsLatest = true - override fun popularMangaRequest(page: Int): Request { + // ============================== Popular ============================== + + override suspend fun getPopularManga(page: Int): MangasPage { val url = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", PAGE_SIZE.toString()) .addQueryParameter("key", API_KEY) @@ -32,10 +33,7 @@ abstract class MangaCloud : HttpSource() { if (token != null) addQueryParameter("pageToken", token) } .build() - return GET(url, headers) - } - - override fun popularMangaParse(response: Response): MangasPage { + val response = client.get(url) val json = FirestoreParser.parseList(response) lastPageToken = json.nextPageToken if (allMangasCache.isEmpty()) { @@ -44,42 +42,38 @@ abstract class MangaCloud : HttpSource() { return MangasPage(json.mangas.map { it.smanga }, json.nextPageToken != null) } - override fun latestUpdatesRequest(page: Int): Request { + // ============================== Latest =============================== + + override suspend fun getLatestUpdates(page: Int): MangasPage { val url = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", PAGE_SIZE.toString()) .addQueryParameter("key", API_KEY) .addQueryParameter("orderBy", "updatedAt desc") .build() - return GET(url, headers) - } - - override fun latestUpdatesParse(response: Response): MangasPage { + val response = client.get(url) val json = FirestoreParser.parseList(response) return MangasPage(json.mangas.map { it.smanga }, false) } - override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - currentSearchQuery = query - currentSortFilter = filters.firstInstanceOrNull()?.selected.orEmpty() - currentGenreFilter = filters.firstInstanceOrNull()?.selected.orEmpty() - currentStatusFilter = filters.firstInstanceOrNull()?.selected.orEmpty() + // ============================== Search =============================== + + override suspend fun getSearchMangaList(page: Int, query: String, filters: FilterList): MangasPage { + val sort = filters.firstInstanceOrNull()?.selected.orEmpty() + val genre = filters.firstInstanceOrNull()?.selected.orEmpty() + val status = filters.firstInstanceOrNull()?.selected.orEmpty() + val url = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", "300") .addQueryParameter("key", API_KEY) .apply { - when (currentSortFilter) { + when (sort) { "new" -> addQueryParameter("orderBy", "createdAt desc") "updated" -> addQueryParameter("orderBy", "updatedAt desc") } } .build() - return GET(url, headers) - } - override fun searchMangaParse(response: Response): MangasPage { - val query = currentSearchQuery.trim() - val genre = currentGenreFilter - val status = currentStatusFilter + val response = client.get(url) val json = FirestoreParser.parseList(response) val allMangas = json.mangas.toMutableList() @@ -89,12 +83,12 @@ abstract class MangaCloud : HttpSource() { var nextPageToken = json.nextPageToken while (nextPageToken != null) { - val url = FIRESTORE_URL.toHttpUrl().newBuilder() + val nextUrl = FIRESTORE_URL.toHttpUrl().newBuilder() .addQueryParameter("pageSize", "300") .addQueryParameter("key", API_KEY) .addQueryParameter("pageToken", nextPageToken) .build() - val nextResponse = client.newCall(GET(url, headers)).execute() + val nextResponse = client.get(nextUrl) val nextJson = FirestoreParser.parseList(nextResponse) allMangas.addAll(nextJson.mangas) allMangasCache.addAll(nextJson.mangas) @@ -126,51 +120,67 @@ abstract class MangaCloud : HttpSource() { return MangasPage(filtered.map { it.smanga }, false) } - override fun mangaDetailsRequest(manga: SManga): Request { - val url = "$FIRESTORE_URL/${manga.url}?key=$API_KEY".toHttpUrl() - return GET(url, headers) + // ============================== Details ============================== + + override suspend fun getMangaByUrl(url: HttpUrl): SManga? { + val mangaId = url.pathSegments.lastOrNull() ?: return null + val firestoreUrl = "$FIRESTORE_URL/$mangaId?key=$API_KEY".toHttpUrl() + val response = client.get(firestoreUrl) + return FirestoreParser.parseDetails(response) } - override fun mangaDetailsParse(response: Response): SManga = FirestoreParser.parseDetails(response) + override suspend fun fetchMangaUpdate( + manga: SManga, + chapters: List, + fetchDetails: Boolean, + fetchChapters: Boolean, + ): SMangaUpdate { + val mangaDetail = if (fetchDetails) { + val url = "$FIRESTORE_URL/${manga.url}?key=$API_KEY".toHttpUrl() + val response = client.get(url) + FirestoreParser.parseDetails(response) + } else { + manga + } - override fun chapterListRequest(manga: SManga): Request { - val url = "$FIRESTORE_URL/${manga.url}/chapters?pageSize=1000&key=$API_KEY&orderBy=index".toHttpUrl() - return GET(url, headers) + val chapterList = if (fetchChapters) { + fetchAllChapters(manga.url) + } else { + chapters + } + + return SMangaUpdate(mangaDetail, chapterList) } - override fun chapterListParse(response: Response): List { + private suspend fun fetchAllChapters(mangaId: String): List { val allChapters = mutableListOf() var nextPageToken: String? = null - var currentResponse = response do { - val jsonString = currentResponse.body?.string() ?: break + val urlBuilder = "$FIRESTORE_URL/$mangaId/chapters".toHttpUrl().newBuilder() + .addQueryParameter("pageSize", "1000") + .addQueryParameter("key", API_KEY) + .addQueryParameter("orderBy", "index") + if (nextPageToken != null) { + urlBuilder.addQueryParameter("pageToken", nextPageToken) + } + val response = client.get(urlBuilder.build()) + val jsonString = response.body?.string() ?: break val json = jsonInstance.parseToJsonElement(jsonString).jsonObject - - val newResponse = currentResponse.newBuilder() - .body(jsonString.toResponseBody(null)) - .build() - val chapters = FirestoreParser.parseChapters(newResponse) + val chaptersResponse = jsonInstance.decodeFromString(jsonString) + val chapters = chaptersResponse.documents?.mapNotNull { doc -> + val docName = doc.name ?: return@mapNotNull null + val chapterNum = docName.substringAfterLast("/") + val num = chapterNum.toIntOrNull() ?: 0 + SChapter.create().apply { + url = docName.substringAfterLast("cloudmangas/") + name = "الفصل $num" + chapter_number = num.toFloat() + } + } ?: emptyList() allChapters.addAll(chapters) - nextPageToken = json["nextPageToken"]?.jsonPrimitive?.contentOrNull - - if (nextPageToken != null) { - val pathSegments = currentResponse.request.url.pathSegments - val chaptersIndex = pathSegments.indexOf("chapters") - if (chaptersIndex < 1) break - val mangaId = pathSegments[chaptersIndex - 1] - - val urlBuilder = "$FIRESTORE_URL/$mangaId/chapters".toHttpUrl().newBuilder() - .addQueryParameter("pageSize", "1000") - .addQueryParameter("key", API_KEY) - .addQueryParameter("orderBy", "index") - .addQueryParameter("pageToken", nextPageToken) - - val newRequest = GET(urlBuilder.build(), headers) - currentResponse = client.newCall(newRequest).execute() - } - } while (nextPageToken != null && currentResponse.isSuccessful) + } while (nextPageToken != null) allChapters.sortByDescending { it.chapter_number?.toString()?.toFloatOrNull() ?: 0f @@ -179,14 +189,15 @@ abstract class MangaCloud : HttpSource() { return allChapters } - override fun pageListRequest(chapter: SChapter): Request { + // ============================== Pages =============================== + + override suspend fun getPageList(chapter: SChapter): List { val url = "${FIRESTORE_URL}/${chapter.url}?key=$API_KEY".toHttpUrl() - return GET(url, headers) + val response = client.get(url) + return FirestoreParser.parsePages(response) } - override fun pageListParse(response: Response): List = FirestoreParser.parsePages(response) - - override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException() + // ============================== URLs ================================ override fun getMangaUrl(manga: SManga): String = "$baseUrl/manga/${manga.url}" @@ -198,17 +209,22 @@ abstract class MangaCloud : HttpSource() { return "$baseUrl/manga/${chapter.url}" } - override fun getFilterList(): FilterList = FilterList( + // ============================== Filters ============================= + + override fun getFilterList(data: kotlinx.serialization.json.JsonElement?) = FilterList( SortFilter("ترتيب حسب", sortingList), StatusFilter("الحالة", statusList), GenreFilter("التصنيف", genreList), ) + // ============================== Related ============================= + + override val supportsRelatedMangas get() = false + override suspend fun fetchRelatedMangaList(manga: SManga): List = emptyList() + + // ============================== State =============================== + private var lastPageToken: String? = null - private var currentSearchQuery: String = "" - private var currentSortFilter: String = "" - private var currentGenreFilter: String = "" - private var currentStatusFilter: String = "" private val allMangasCache = mutableListOf() companion object { From edb79424df6c2289061d49e7ee5d8e55f09a2153 Mon Sep 17 00:00:00 2001 From: yemen Date: Fri, 17 Jul 2026 17:34:51 +0300 Subject: [PATCH 24/24] fix(ar/mangacloud): use parseAs, remove defaults, add async parallel fetch, validate mangaId --- .../tachiyomi/extension/ar/mangacloud/Dto.kt | 15 +++--- .../extension/ar/mangacloud/MangaCloud.kt | 51 ++++++++----------- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt index 13af9303418..e6a3305e8f0 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/Dto.kt @@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.extension.ar.mangacloud import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import keiyoushi.utils.jsonInstance +import keiyoushi.utils.parseAs import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.contentOrNull @@ -39,7 +39,7 @@ data class MangaListResult( object FirestoreParser { fun parseList(response: Response): MangaListResult { - val result = jsonInstance.decodeFromString(response.body.string()) + val result = response.parseAs() val mangas = result.documents?.mapNotNull { doc -> val fields = doc.fields ?: return@mapNotNull null val mangaId = doc.name?.substringAfterLast("/") ?: return@mapNotNull null @@ -73,9 +73,10 @@ object FirestoreParser { } fun parseDetails(response: Response): SManga { - val doc = jsonInstance.decodeFromString(response.body.string()) - val fields = doc.fields ?: return SManga.create() - val mangaId = doc.name?.substringAfterLast("/") ?: "" + val doc = response.parseAs() + val fields = doc.fields!! + val mangaId = doc.name!!.substringAfterLast("/") + check(mangaId.isNotEmpty()) { "mangaId must not be empty" } return SManga.create().apply { url = mangaId title = fields.string("title") @@ -94,7 +95,7 @@ object FirestoreParser { } fun parseChapters(response: Response): List { - val result = jsonInstance.decodeFromString(response.body.string()) + val result = response.parseAs() return result.documents?.mapNotNull { doc -> val docName = doc.name ?: return@mapNotNull null val chapterNum = docName.substringAfterLast("/") @@ -108,7 +109,7 @@ object FirestoreParser { } fun parsePages(response: Response): List { - val doc = jsonInstance.decodeFromString(response.body.string()) + val doc = response.parseAs() val fields = doc.fields ?: return emptyList() val pagesArray = fields["pages"]?.jsonObject?.get("arrayValue")?.jsonObject ?.get("values")?.jsonArray ?: return emptyList() diff --git a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt index b60f72847df..d21f19d6e33 100644 --- a/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt +++ b/src/ar/mangacloud/src/eu/kanade/tachiyomi/extension/ar/mangacloud/MangaCloud.kt @@ -10,18 +10,15 @@ import keiyoushi.annotation.Source import keiyoushi.network.get import keiyoushi.source.KeiSource import keiyoushi.utils.firstInstanceOrNull -import keiyoushi.utils.jsonInstance -import kotlinx.serialization.json.contentOrNull -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive +import keiyoushi.utils.parseAs +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl @Source abstract class MangaCloud : KeiSource() { - override val supportsLatest = true - // ============================== Popular ============================== override suspend fun getPopularManga(page: Int): MangasPage { @@ -36,9 +33,6 @@ abstract class MangaCloud : KeiSource() { val response = client.get(url) val json = FirestoreParser.parseList(response) lastPageToken = json.nextPageToken - if (allMangasCache.isEmpty()) { - allMangasCache.addAll(json.mangas) - } return MangasPage(json.mangas.map { it.smanga }, json.nextPageToken != null) } @@ -77,10 +71,6 @@ abstract class MangaCloud : KeiSource() { val json = FirestoreParser.parseList(response) val allMangas = json.mangas.toMutableList() - if (allMangasCache.isEmpty()) { - allMangasCache.addAll(json.mangas) - } - var nextPageToken = json.nextPageToken while (nextPageToken != null) { val nextUrl = FIRESTORE_URL.toHttpUrl().newBuilder() @@ -91,7 +81,6 @@ abstract class MangaCloud : KeiSource() { val nextResponse = client.get(nextUrl) val nextJson = FirestoreParser.parseList(nextResponse) allMangas.addAll(nextJson.mangas) - allMangasCache.addAll(nextJson.mangas) nextPageToken = nextJson.nextPageToken } @@ -134,22 +123,27 @@ abstract class MangaCloud : KeiSource() { chapters: List, fetchDetails: Boolean, fetchChapters: Boolean, - ): SMangaUpdate { - val mangaDetail = if (fetchDetails) { - val url = "$FIRESTORE_URL/${manga.url}?key=$API_KEY".toHttpUrl() - val response = client.get(url) - FirestoreParser.parseDetails(response) + ): SMangaUpdate = coroutineScope { + val mangaDetailDeferred = if (fetchDetails) { + async { + val url = "$FIRESTORE_URL/${manga.url}?key=$API_KEY".toHttpUrl() + val response = client.get(url) + FirestoreParser.parseDetails(response) + } } else { - manga + null } - val chapterList = if (fetchChapters) { - fetchAllChapters(manga.url) + val chapterListDeferred = if (fetchChapters) { + async { fetchAllChapters(manga.url) } } else { - chapters + null } - return SMangaUpdate(mangaDetail, chapterList) + val mangaDetail = mangaDetailDeferred?.await() ?: manga + val chapterList = chapterListDeferred?.await() ?: chapters + + SMangaUpdate(mangaDetail, chapterList) } private suspend fun fetchAllChapters(mangaId: String): List { @@ -165,10 +159,8 @@ abstract class MangaCloud : KeiSource() { urlBuilder.addQueryParameter("pageToken", nextPageToken) } val response = client.get(urlBuilder.build()) - val jsonString = response.body?.string() ?: break - val json = jsonInstance.parseToJsonElement(jsonString).jsonObject - val chaptersResponse = jsonInstance.decodeFromString(jsonString) - val chapters = chaptersResponse.documents?.mapNotNull { doc -> + val result = response.parseAs() + val chapters = result.documents?.mapNotNull { doc -> val docName = doc.name ?: return@mapNotNull null val chapterNum = docName.substringAfterLast("/") val num = chapterNum.toIntOrNull() ?: 0 @@ -179,7 +171,7 @@ abstract class MangaCloud : KeiSource() { } } ?: emptyList() allChapters.addAll(chapters) - nextPageToken = json["nextPageToken"]?.jsonPrimitive?.contentOrNull + nextPageToken = result.nextPageToken } while (nextPageToken != null) allChapters.sortByDescending { @@ -225,7 +217,6 @@ abstract class MangaCloud : KeiSource() { // ============================== State =============================== private var lastPageToken: String? = null - private val allMangasCache = mutableListOf() companion object { private const val API_KEY = "AIzaSyAvdmgz_r_d89Eo8JBs9vAjUAJR451bMYU"