From 0da807ff7068aef5f219e2f72a13b83438d7c24e Mon Sep 17 00:00:00 2001 From: Tim Schneeberger Date: Sun, 27 Apr 2025 04:07:47 +0200 Subject: [PATCH] feat: add GlobalComix (#8637) * feat: add GlobalComix Closes #3726 * fix: parse comic URLs correctly * style: cleanup * refactor: rename PageListDataDto to PageDataDto * fix: sort search results by relevancy * fix: improve premium chapter detection * refactor: add chapter number to SChapter * refactor: remove unused fields and allow some fields to be nullable * refactor: minor cleanup * Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComix.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComix.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComix.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * Update src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ChapterDto.kt Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * refactor: remove CacheControl * refactor: move constants of out object * refactor: add new imports & remove 204 check * refactor: remove chapter list 204 check --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- src/all/globalcomix/AndroidManifest.xml | 27 ++ .../assets/i18n/messages_en.properties | 5 + src/all/globalcomix/build.gradle | 12 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4418 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2599 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 5378 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 8982 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 12900 bytes .../extension/all/globalcomix/GlobalComix.kt | 234 ++++++++++++++++++ .../all/globalcomix/GlobalComixConstants.kt | 30 +++ .../all/globalcomix/GlobalComixFactory.kt | 92 +++++++ .../all/globalcomix/GlobalComixUrlActivity.kt | 45 ++++ .../all/globalcomix/dto/ArtistDto.kt | 13 + .../all/globalcomix/dto/ChapterDto.kt | 63 +++++ .../all/globalcomix/dto/EntityDto.kt | 11 + .../extension/all/globalcomix/dto/MangaDto.kt | 49 ++++ .../all/globalcomix/dto/PageDataDto.kt | 14 ++ .../globalcomix/dto/PaginatedResponseDto.kt | 36 +++ 18 files changed, 631 insertions(+) create mode 100644 src/all/globalcomix/AndroidManifest.xml create mode 100644 src/all/globalcomix/assets/i18n/messages_en.properties create mode 100644 src/all/globalcomix/build.gradle create mode 100644 src/all/globalcomix/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/all/globalcomix/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/all/globalcomix/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/all/globalcomix/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/all/globalcomix/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComix.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixConstants.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixFactory.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixUrlActivity.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ArtistDto.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ChapterDto.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/EntityDto.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/MangaDto.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PageDataDto.kt create mode 100644 src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PaginatedResponseDto.kt diff --git a/src/all/globalcomix/AndroidManifest.xml b/src/all/globalcomix/AndroidManifest.xml new file mode 100644 index 000000000..a58173090 --- /dev/null +++ b/src/all/globalcomix/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/all/globalcomix/assets/i18n/messages_en.properties b/src/all/globalcomix/assets/i18n/messages_en.properties new file mode 100644 index 000000000..af9af7967 --- /dev/null +++ b/src/all/globalcomix/assets/i18n/messages_en.properties @@ -0,0 +1,5 @@ +data_saver=Data saver +data_saver_summary=Enables smaller, more compressed images +invalid_manga_id=Not a valid comic ID +show_locked_chapters=Show chapters with pay-walled pages +show_locked_chapters_summary=Display chapters that require an account with a premium subscription diff --git a/src/all/globalcomix/build.gradle b/src/all/globalcomix/build.gradle new file mode 100644 index 000000000..637969339 --- /dev/null +++ b/src/all/globalcomix/build.gradle @@ -0,0 +1,12 @@ +ext { + extName = 'GlobalComix' + extClass = '.GlobalComixFactory' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" + +dependencies { + implementation(project(":lib:i18n")) +} diff --git a/src/all/globalcomix/res/mipmap-hdpi/ic_launcher.png b/src/all/globalcomix/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..895825c1200a26b67ca6c19215133b29af5f6c11 GIT binary patch literal 4418 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 IM6N<$g2ZZ2iU0rr literal 0 HcmV?d00001 diff --git a/src/all/globalcomix/res/mipmap-mdpi/ic_launcher.png b/src/all/globalcomix/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..6d7b6fdb36e0a328c7cd32f49ba33a734d6db9c3 GIT binary patch literal 2599 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 literal 0 HcmV?d00001 diff --git a/src/all/globalcomix/res/mipmap-xxhdpi/ic_launcher.png b/src/all/globalcomix/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ef35b26b7b104b6404d3ea3bab38f754d1b5cdc8 GIT binary patch literal 8982 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 Intl.createDefaultMessageFileName(lang) }, + ) + + final override fun headersBuilder() = super.headersBuilder().apply { + set("Referer", "$baseUrl/") + set("Origin", baseUrl) + set("x-gc-client", clientId) + set("x-gc-identmode", "cookie") + } + + override val client = network.client.newBuilder() + .rateLimit(3) + .build() + + private fun simpleQueryRequest(page: Int, orderBy: String?, query: String?): Request { + val url = apiSearchUrl.toHttpUrl().newBuilder() + .addQueryParameter("lang_id[]", extLang) + .addQueryParameter("p", page.toString()) + + orderBy?.let { url.addQueryParameter("sort", it) } + query?.let { url.addQueryParameter("q", it) } + + return GET(url.build(), headers) + } + + override fun popularMangaRequest(page: Int): Request = + simpleQueryRequest(page, orderBy = null, query = null) + + override fun popularMangaParse(response: Response): MangasPage = + mangaListParse(response) + + override fun latestUpdatesRequest(page: Int): Request = + simpleQueryRequest(page, "recent", query = null) + + override fun latestUpdatesParse(response: Response): MangasPage = + mangaListParse(response) + + private fun mangaListParse(response: Response): MangasPage { + val isSingleItemLookup = response.request.url.toString().startsWith(apiMangaUrl) + return if (!isSingleItemLookup) { + // Normally, the response is a paginated list of mangas + // The results property will be a JSON array + response.parseAs().payload!!.let { dto -> + MangasPage( + dto.results.map { it -> it.createManga() }, + dto.pagination.hasNextPage, + ) + } + } else { + // However, when using the 'id:' query prefix (via the UrlActivity for example), + // the response is a single manga and the results property will be a JSON object + MangasPage( + listOf( + response.parseAs().payload!! + .results + .createManga(), + ), + false, + ) + } + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + // If the query is a slug ID, return the manga directly + if (query.startsWith(prefixIdSearch)) { + val mangaSlugId = query.removePrefix(prefixIdSearch) + + if (mangaSlugId.isEmpty()) { + throw Exception(intl["invalid_manga_id"]) + } + + val url = apiMangaUrl.toHttpUrl().newBuilder() + .addPathSegment(mangaSlugId) + .build() + + return GET(url, headers) + } + + return simpleQueryRequest(page, orderBy = "relevance", query) + } + + override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response) + + override fun getMangaUrl(manga: SManga): String = "$webComicUrl/${titleToSlug(manga.title)}" + + override fun mangaDetailsRequest(manga: SManga): Request { + val url = apiMangaUrl.toHttpUrl().newBuilder() + .addPathSegment(titleToSlug(manga.title)) + .build() + + return GET(url, headers) + } + + override fun mangaDetailsParse(response: Response): SManga = + response.parseAs().payload!! + .results + .createManga() + + override fun chapterListRequest(manga: SManga): Request { + val url = apiSearchUrl.toHttpUrl().newBuilder() + .addPathSegment(manga.url) // manga.url contains the the comic id + .addPathSegment("releases") + .addQueryParameter("lang_id", extLang) + .addQueryParameter("all", "true") + .toString() + + return GET(url, headers) + } + + override fun chapterListParse(response: Response): List = + response.parseAs().payload!!.results.filterNot { dto -> + dto.isPremium && !preferences.showLockedChapters + }.map { it.createChapter() } + + override fun getChapterUrl(chapter: SChapter): String = + "$baseUrl/read/${chapter.url}" + + override fun pageListRequest(chapter: SChapter): Request { + val chapterKey = chapter.url + val url = "$apiChapterUrl/$chapterKey" + return GET(url, headers) + } + + override fun pageListParse(response: Response): List { + val chapterKey = response.request.url.pathSegments.last() + val chapterWebUrl = "$webChapterUrl/$chapterKey" + + return response.parseAs() + .payload!! + .results + .page_objects!! + .map { dto -> if (preferences.useDataSaver) dto.mobile_image_url else dto.desktop_image_url } + .mapIndexed { index, url -> Page(index, "$chapterWebUrl/$index", url) } + } + + override fun imageUrlParse(response: Response): String = "" + + override fun setupPreferenceScreen(screen: PreferenceScreen) { + val dataSaverPref = SwitchPreferenceCompat(screen.context).apply { + key = getDataSaverPreferenceKey(extLang) + title = intl["data_saver"] + summary = intl["data_saver_summary"] + setDefaultValue(false) + } + + val showLockedChaptersPref = SwitchPreferenceCompat(screen.context).apply { + key = getShowLockedChaptersPreferenceKey(extLang) + title = intl["show_locked_chapters"] + summary = intl["show_locked_chapters_summary"] + setDefaultValue(true) + } + + screen.addPreference(dataSaverPref) + screen.addPreference(showLockedChaptersPref) + } + + private inline fun Response.parseAs(): T = parseAs(json) + + private val SharedPreferences.useDataSaver + get() = getBoolean(getDataSaverPreferenceKey(extLang), false) + + private val SharedPreferences.showLockedChapters + get() = getBoolean(getShowLockedChaptersPreferenceKey(extLang), true) + + companion object { + fun titleToSlug(title: String) = title.trim() + .lowercase(Locale.US) + .replace(titleSpecialCharactersRegex, "-") + + val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex() + val dateFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) + .apply { timeZone = TimeZone.getTimeZone("UTC") } + } +} diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixConstants.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixConstants.kt new file mode 100644 index 000000000..93807cff6 --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixConstants.kt @@ -0,0 +1,30 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix + +const val lockSymbol = "🔒" + +// Language codes used for translations +const val english = "en" + +// JSON discriminators +const val release = "Release" +const val comic = "Comic" +const val artist = "Artist" +const val releasePage = "ReleasePage" + +// Web requests +const val webUrl = "https://globalcomix.com" +const val webComicUrl = "$webUrl/c" +const val webChapterUrl = "$webUrl/read" +const val apiUrl = "https://api.globalcomix.com/v1" +const val apiMangaUrl = "$apiUrl/read" +const val apiChapterUrl = "$apiUrl/readV2" +const val apiSearchUrl = "$apiUrl/comics" + +const val clientId = "gck_d0f170d5729446dcb3b55e6b3ebc7bf6" + +// Search prefix for title ids +const val prefixIdSearch = "id:" + +// Preferences +fun getDataSaverPreferenceKey(extLang: String): String = "dataSaver_$extLang" +fun getShowLockedChaptersPreferenceKey(extLang: String): String = "showLockedChapters_$extLang" diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixFactory.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixFactory.kt new file mode 100644 index 000000000..39cf5c950 --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixFactory.kt @@ -0,0 +1,92 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class GlobalComixFactory : SourceFactory { + override fun createSources(): List = listOf( + GlobalComixAlbanian(), + GlobalComixArabic(), + GlobalComixBulgarian(), + GlobalComixBengali(), + GlobalComixBrazilianPortuguese(), + GlobalComixChineseMandarin(), + GlobalComixCzech(), + GlobalComixGerman(), + GlobalComixDanish(), + GlobalComixGreek(), + GlobalComixEnglish(), + GlobalComixSpanish(), + GlobalComixPersian(), + GlobalComixFinnish(), + GlobalComixFilipino(), + GlobalComixFrench(), + GlobalComixHindi(), + GlobalComixHungarian(), + GlobalComixIndonesian(), + GlobalComixItalian(), + GlobalComixHebrew(), + GlobalComixJapanese(), + GlobalComixKorean(), + GlobalComixLatvian(), + GlobalComixMalay(), + GlobalComixDutch(), + GlobalComixNorwegian(), + GlobalComixPolish(), + GlobalComixPortugese(), + GlobalComixRomanian(), + GlobalComixRussian(), + GlobalComixSwedish(), + GlobalComixSlovak(), + GlobalComixSlovenian(), + GlobalComixTamil(), + GlobalComixThai(), + GlobalComixTurkish(), + GlobalComixUkrainian(), + GlobalComixUrdu(), + GlobalComixVietnamese(), + GlobalComixChineseCantonese(), + ) +} + +class GlobalComixAlbanian : GlobalComix("al") +class GlobalComixArabic : GlobalComix("ar") +class GlobalComixBulgarian : GlobalComix("bg") +class GlobalComixBengali : GlobalComix("bn") +class GlobalComixBrazilianPortuguese : GlobalComix("pt-BR", "br") +class GlobalComixChineseMandarin : GlobalComix("zh-Hans", "cn") +class GlobalComixCzech : GlobalComix("cs", "cz") +class GlobalComixGerman : GlobalComix("de") +class GlobalComixDanish : GlobalComix("dk") +class GlobalComixGreek : GlobalComix("el") +class GlobalComixEnglish : GlobalComix("en") +class GlobalComixSpanish : GlobalComix("es") +class GlobalComixPersian : GlobalComix("fa") +class GlobalComixFinnish : GlobalComix("fi") +class GlobalComixFilipino : GlobalComix("fil", "fo") +class GlobalComixFrench : GlobalComix("fr") +class GlobalComixHindi : GlobalComix("hi") +class GlobalComixHungarian : GlobalComix("hu") +class GlobalComixIndonesian : GlobalComix("id") +class GlobalComixItalian : GlobalComix("it") +class GlobalComixHebrew : GlobalComix("he", "iw") +class GlobalComixJapanese : GlobalComix("ja", "jp") +class GlobalComixKorean : GlobalComix("ko", "kr") +class GlobalComixLatvian : GlobalComix("lv") +class GlobalComixMalay : GlobalComix("ms", "my") +class GlobalComixDutch : GlobalComix("nl") +class GlobalComixNorwegian : GlobalComix("no") +class GlobalComixPolish : GlobalComix("pl") +class GlobalComixPortugese : GlobalComix("pt") +class GlobalComixRomanian : GlobalComix("ro") +class GlobalComixRussian : GlobalComix("ru") +class GlobalComixSwedish : GlobalComix("sv", "se") +class GlobalComixSlovak : GlobalComix("sk") +class GlobalComixSlovenian : GlobalComix("sl") +class GlobalComixTamil : GlobalComix("ta") +class GlobalComixThai : GlobalComix("th") +class GlobalComixTurkish : GlobalComix("tr") +class GlobalComixUkrainian : GlobalComix("uk", "ua") +class GlobalComixUrdu : GlobalComix("ur") +class GlobalComixVietnamese : GlobalComix("vi") +class GlobalComixChineseCantonese : GlobalComix("zh-Hant", "zh") diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixUrlActivity.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixUrlActivity.kt new file mode 100644 index 000000000..5de91fbff --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/GlobalComixUrlActivity.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.Toast +import kotlin.system.exitProcess + +/** + * Springboard that accepts https://globalcomix.com/c/xxx intents and redirects them to + * the main tachiyomi process. The idea is to not install the intent filter unless + * you have this extension installed, but still let the main tachiyomi app control + * things. + */ +class GlobalComixUrlActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val pathSegments = intent?.data?.pathSegments + + // Supported path: /c/title-slug + if (pathSegments != null && pathSegments.size > 1) { + val titleId = pathSegments[1] + val mainIntent = Intent().apply { + action = "eu.kanade.tachiyomi.SEARCH" + putExtra("query", prefixIdSearch + titleId) + putExtra("filter", packageName) + } + + try { + startActivity(mainIntent) + } catch (e: ActivityNotFoundException) { + Log.e("GlobalComixUrlActivity", e.toString()) + } + } else { + Log.e("GlobalComixUrlActivity", "Received data URL is unsupported: ${intent?.data}") + Toast.makeText(this, "This URL cannot be handled by the GlobalComix extension.", Toast.LENGTH_SHORT).show() + } + + finish() + exitProcess(0) + } +} diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ArtistDto.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ArtistDto.kt new file mode 100644 index 000000000..96f3da9e3 --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ArtistDto.kt @@ -0,0 +1,13 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix.dto + +import eu.kanade.tachiyomi.extension.all.globalcomix.artist +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Suppress("PropertyName") +@Serializable +@SerialName(artist) +class ArtistDto( + val name: String, // Slug + val roman_name: String?, +) : EntityDto() diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ChapterDto.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ChapterDto.kt new file mode 100644 index 000000000..adb8570f4 --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/ChapterDto.kt @@ -0,0 +1,63 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix.dto + +import eu.kanade.tachiyomi.extension.all.globalcomix.GlobalComix.Companion.dateFormatter +import eu.kanade.tachiyomi.extension.all.globalcomix.lockSymbol +import eu.kanade.tachiyomi.extension.all.globalcomix.release +import eu.kanade.tachiyomi.source.model.SChapter +import keiyoushi.utils.tryParse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +typealias ChapterDto = ResponseDto +typealias ChaptersDto = PaginatedResponseDto + +@Suppress("PropertyName") +@Serializable +@SerialName(release) +class ChapterDataDto( + val title: String, + val chapter: String, // Stringified number + val key: String, // UUID, required for /readV2 endpoint + val premium_only: Int? = 0, + val published_time: String, + + // Only available when calling the /readV2 endpoint + val page_objects: List?, +) : EntityDto() { + val isPremium: Boolean + get() = premium_only == 1 + + companion object { + /** + * Create an [SChapter] instance from the JSON DTO element. + */ + fun ChapterDataDto.createChapter(): SChapter { + val chapterName = mutableListOf() + if (isPremium) { + chapterName.add(lockSymbol) + } + + chapter.let { + if (it.isNotEmpty()) { + chapterName.add("Ch.$it") + } + } + + title.let { + if (it.isNotEmpty()) { + if (chapterName.isNotEmpty()) { + chapterName.add("-") + } + chapterName.add(it) + } + } + + return SChapter.create().apply { + url = key + name = chapterName.joinToString(" ") + chapter_number = chapter.toFloatOrNull() ?: 0f + date_upload = dateFormatter.tryParse(published_time) + } + } + } +} diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/EntityDto.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/EntityDto.kt new file mode 100644 index 000000000..c74fe9f4f --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/EntityDto.kt @@ -0,0 +1,11 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix.dto + +import kotlinx.serialization.Serializable + +@Serializable +sealed class EntityDto { + val id: Long = -1 +} + +@Serializable +class UnknownEntity() : EntityDto() diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/MangaDto.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/MangaDto.kt new file mode 100644 index 000000000..5cc54bd98 --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/MangaDto.kt @@ -0,0 +1,49 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix.dto + +import eu.kanade.tachiyomi.extension.all.globalcomix.comic +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +typealias MangaDto = ResponseDto +typealias MangasDto = PaginatedResponseDto + +@Suppress("PropertyName") +@Serializable +@SerialName(comic) +class MangaDataDto( + val name: String, + val description: String?, + val status_name: String?, + val category_name: String?, + val image_url: String?, + val artist: ArtistDto, +) : EntityDto() { + companion object { + /** + * Create an [SManga] instance from the JSON DTO element. + */ + fun MangaDataDto.createManga(): SManga = + SManga.create().also { + it.initialized = true + it.url = id.toString() + it.description = description + it.author = artist.let { it.roman_name ?: it.name } + it.status = status_name?.let(::convertStatus) ?: SManga.UNKNOWN + it.genre = category_name + it.title = name + it.thumbnail_url = image_url + } + + private fun convertStatus(status: String): Int { + return when (status) { + "Ongoing" -> SManga.ONGOING + "Preview" -> SManga.ONGOING + "Finished" -> SManga.COMPLETED + "On hold" -> SManga.ON_HIATUS + "Cancelled" -> SManga.CANCELLED + else -> SManga.UNKNOWN + } + } + } +} diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PageDataDto.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PageDataDto.kt new file mode 100644 index 000000000..921c614ee --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PageDataDto.kt @@ -0,0 +1,14 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix.dto + +import eu.kanade.tachiyomi.extension.all.globalcomix.releasePage +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Suppress("PropertyName") +@Serializable +@SerialName(releasePage) +class PageDataDto( + val is_page_paid: Boolean, + val desktop_image_url: String, + val mobile_image_url: String, +) : EntityDto() diff --git a/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PaginatedResponseDto.kt b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PaginatedResponseDto.kt new file mode 100644 index 000000000..9b3d72bca --- /dev/null +++ b/src/all/globalcomix/src/eu/kanade/tachiyomi/extension/all/globalcomix/dto/PaginatedResponseDto.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.extension.all.globalcomix.dto + +import kotlinx.serialization.Serializable + +@Serializable +class PaginatedResponseDto( + val payload: PaginatedPayloadDto? = null, +) + +@Serializable +class PaginatedPayloadDto( + val results: List = emptyList(), + val pagination: PaginationStateDto, +) + +@Serializable +class ResponseDto( + val payload: PayloadDto? = null, +) + +@Serializable +class PayloadDto( + val results: T, +) + +@Suppress("PropertyName") +@Serializable +class PaginationStateDto( + val page: Int = 1, + val per_page: Int = 0, + val total_pages: Int = 0, + val total_results: Int = 0, +) { + val hasNextPage: Boolean + get() = page < total_pages +}