Add NamiComi (#7057)
* Add Namicomi
* Remove conditional; already handled by the intent-filter
* Simplify error handling
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
* Update old comment
* Remove hardcoded /en/ url path
* Chapters missing from the accessMap should be considered inaccessible
* Remove setOnPreferenceChangeListener
* Remove unused i18n key
* Rename Namicomi to NamiComi
* Revert accidental change to settings.gradle.kts
* Remove remaining setOnPreferenceListener
* Close response on error
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
* Require nonnull chapter access map
* Change abstract to sealed in EntityDto
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
* Change abstract to sealed in MangaDto
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
* Change SManga.PUBLISHING_FINISHED to SManga.COMPLETED
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
* Set initialized = true
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
* Remove allowSpecialFloatingPointValues and prettyPrint
* Remove markdown cleanup functions
* Cleanup import
* Remove constructors for new sealed interface
* Fix PaginatedResponseDto structure
* Simplify and remove createBasicManga
* Remove old MangaDex code
* Use 🔒 for locked chapters
* Update NamiComiHelper.kt
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
* Remove data modifier from dto classes
* Apply suggestions from code review
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
* Update URL/ID handling
* Move companion object to bottom
---------
Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com>
Co-authored-by: FourTOne5 <107297513+FourTOne5@users.noreply.github.com>
This commit is contained in:
parent
a837998ad8
commit
d470490087
src/all/namicomi
AndroidManifest.xml
assets/i18n
build.gradleres
mipmap-hdpi
mipmap-mdpi
mipmap-xhdpi
mipmap-xxhdpi
mipmap-xxxhdpi
src/eu/kanade/tachiyomi/extension/all/namicomi
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name=".all.namicomi.NamiComiUrlActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter
|
||||||
|
android:autoVerify="false"
|
||||||
|
tools:targetApi="23">
|
||||||
|
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:host="namicomi.com" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
|
||||||
|
<data android:pathPattern="/.*/title/..*" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
|
@ -0,0 +1,114 @@
|
||||||
|
content=Content
|
||||||
|
content_rating=Content rating
|
||||||
|
content_rating_genre=Content rating: %s
|
||||||
|
content_rating_mature=Mature
|
||||||
|
content_rating_restricted=Restricted
|
||||||
|
content_rating_safe=Safe
|
||||||
|
content_warnings_drugs=Drugs
|
||||||
|
content_warnings_gambling=Gambling
|
||||||
|
content_warnings_gore=Gore
|
||||||
|
content_warnings_mental_disorders=Mental Disorders
|
||||||
|
content_warnings_physical_abuse=Physical Abuse
|
||||||
|
content_warnings_racism=Racism
|
||||||
|
content_warnings_self_harm=Self-harm
|
||||||
|
content_warnings_sexual_abuse=Sexual Abuse
|
||||||
|
content_warnings_verbal_abuse=Verbal Abuse
|
||||||
|
cover_quality=Cover quality
|
||||||
|
cover_quality_low=Low
|
||||||
|
cover_quality_medium=Medium
|
||||||
|
cover_quality_original=Original
|
||||||
|
data_saver=Data saver
|
||||||
|
data_saver_summary=Enables smaller, more compressed images
|
||||||
|
error_payment_required=Payment required. Chapter requires a premium subscription
|
||||||
|
excluded_tags_mode=Excluded tags mode
|
||||||
|
format=Format
|
||||||
|
format_4_koma=4-Koma
|
||||||
|
format_adaptation=Adaptation
|
||||||
|
format_anthology=Anthology
|
||||||
|
format_full_color=Full Color
|
||||||
|
format_oneshot=Oneshot
|
||||||
|
format_silent=Silent
|
||||||
|
genre=Genre
|
||||||
|
genre_action=Action
|
||||||
|
genre_adventure=Adventure
|
||||||
|
genre_boys_love=Boys' Love
|
||||||
|
genre_comedy=Comedy
|
||||||
|
genre_crime=Crime
|
||||||
|
genre_drama=Drama
|
||||||
|
genre_fantasy=Fantasy
|
||||||
|
genre_girls_love=Girls' Love
|
||||||
|
genre_historical=Historical
|
||||||
|
genre_horror=Horror
|
||||||
|
genre_isekai=Isekai
|
||||||
|
genre_mecha=Mecha
|
||||||
|
genre_medical=Medical
|
||||||
|
genre_mystery=Mystery
|
||||||
|
genre_philosophical=Philosophical
|
||||||
|
genre_psychological=Psychological
|
||||||
|
genre_romance=Romance
|
||||||
|
genre_sci_fi=Sci-Fi
|
||||||
|
genre_slice_of_life=Slice of Life
|
||||||
|
genre_sports=Sports
|
||||||
|
genre_superhero=Superhero
|
||||||
|
genre_thriller=Thriller
|
||||||
|
genre_tragedy=Tragedy
|
||||||
|
genre_wuxia=Wuxia
|
||||||
|
has_available_chapters=Has available chapters
|
||||||
|
included_tags_mode=Included tags mode
|
||||||
|
invalid_manga_id=Not a valid title ID
|
||||||
|
mode_and=And
|
||||||
|
mode_or=Or
|
||||||
|
show_locked_chapters=Show locked/paywalled chapters
|
||||||
|
show_locked_chapters_summary=Display chapters that require an account with a premium subscription
|
||||||
|
sort=Sort
|
||||||
|
sort_alphabetic=Alphabetic
|
||||||
|
sort_content_created_at=Content created at
|
||||||
|
sort_number_of_chapters=Chapter count
|
||||||
|
sort_number_of_comments=Comment count
|
||||||
|
sort_number_of_follows=Followers
|
||||||
|
sort_number_of_likes=Likes
|
||||||
|
sort_rating=Rating
|
||||||
|
sort_views=Views
|
||||||
|
sort_year=Year
|
||||||
|
status=Status
|
||||||
|
status_cancelled=Cancelled
|
||||||
|
status_completed=Completed
|
||||||
|
status_hiatus=Hiatus
|
||||||
|
status_ongoing=Ongoing
|
||||||
|
tags_mode=Tags mode
|
||||||
|
theme=Theme
|
||||||
|
theme_aliens=Aliens
|
||||||
|
theme_animals=Animals
|
||||||
|
theme_cooking=Cooking
|
||||||
|
theme_crossdressing=Crossdressing
|
||||||
|
theme_delinquents=Delinquents
|
||||||
|
theme_demons=Demons
|
||||||
|
theme_genderswap=Genderswap
|
||||||
|
theme_ghosts=Ghosts
|
||||||
|
theme_gyaru=Gyaru
|
||||||
|
theme_harem=Harem
|
||||||
|
theme_mafia=Mafia
|
||||||
|
theme_magic=Magic
|
||||||
|
theme_magical_girls=Magical Girls
|
||||||
|
theme_martial_arts=Martial Arts
|
||||||
|
theme_military=Military
|
||||||
|
theme_monster_girls=Monster Girls
|
||||||
|
theme_monsters=Monsters
|
||||||
|
theme_music=Music
|
||||||
|
theme_ninja=Ninja
|
||||||
|
theme_office_workers=Office Workers
|
||||||
|
theme_police=Police
|
||||||
|
theme_post_apocalyptic=Post-Apocalyptic
|
||||||
|
theme_reincarnation=Reincarnation
|
||||||
|
theme_reverse_harem=Reverse Harem
|
||||||
|
theme_samurai=Samurai
|
||||||
|
theme_school_life=School Life
|
||||||
|
theme_supernatural=Supernatural
|
||||||
|
theme_survival=Survival
|
||||||
|
theme_time_travel=Time Travel
|
||||||
|
theme_traditional_games=Traditional Games
|
||||||
|
theme_vampires=Vampires
|
||||||
|
theme_video_games=Video Games
|
||||||
|
theme_villainess=Villainess
|
||||||
|
theme_virtual_reality=Virtual Reality
|
||||||
|
theme_zombies=Zombies
|
|
@ -0,0 +1,12 @@
|
||||||
|
ext {
|
||||||
|
extName = 'NamiComi'
|
||||||
|
extClass = '.NamiComiFactory'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":lib:i18n"))
|
||||||
|
}
|
Binary file not shown.
After ![]() (image error) Size: 3.3 KiB |
Binary file not shown.
After ![]() (image error) Size: 1.8 KiB |
Binary file not shown.
After ![]() (image error) Size: 4.5 KiB |
Binary file not shown.
After ![]() (image error) Size: 7.9 KiB |
Binary file not shown.
After ![]() (image error) Size: 11 KiB |
|
@ -0,0 +1,354 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import androidx.preference.SwitchPreferenceCompat
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ChapterListDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityAccessMapDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityAccessRequestDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityAccessRequestItemDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.MangaDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.MangaListDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.PageListDto
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
abstract class NamiComi(final override val lang: String, private val extLang: String = lang) :
|
||||||
|
ConfigurableSource, HttpSource() {
|
||||||
|
|
||||||
|
override val name = "NamiComi"
|
||||||
|
override val baseUrl = NamiComiConstants.webUrl
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val helper = NamiComiHelper(lang)
|
||||||
|
|
||||||
|
final override fun headersBuilder() = super.headersBuilder().apply {
|
||||||
|
set("Referer", "$baseUrl/")
|
||||||
|
set("Origin", baseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val client = network.client.newBuilder()
|
||||||
|
.rateLimit(3)
|
||||||
|
.addNetworkInterceptor { chain ->
|
||||||
|
val response = chain.proceed(chain.request())
|
||||||
|
|
||||||
|
if (response.code == 402) {
|
||||||
|
response.close()
|
||||||
|
throw IOException(helper.intl["error_payment_required"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return@addNetworkInterceptor response
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private fun sortedMangaRequest(page: Int, orderBy: String): Request {
|
||||||
|
val url = NamiComiConstants.apiSearchUrl.toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("order[$orderBy]", "desc")
|
||||||
|
.addQueryParameter("availableTranslatedLanguages[]", extLang)
|
||||||
|
.addQueryParameter("limit", NamiComiConstants.mangaLimit.toString())
|
||||||
|
.addQueryParameter("offset", helper.getMangaListOffset(page))
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.coverArt)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.primaryTag)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.secondaryTag)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.tag)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popular manga section
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
|
sortedMangaRequest(page, "views")
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage =
|
||||||
|
mangaListParse(response)
|
||||||
|
|
||||||
|
// Latest manga section
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
|
sortedMangaRequest(page, "publishedAt")
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||||
|
mangaListParse(response)
|
||||||
|
|
||||||
|
private fun mangaListParse(response: Response): MangasPage {
|
||||||
|
if (response.code == 204) {
|
||||||
|
return MangasPage(emptyList(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaListDto = response.parseAs<MangaListDto>()
|
||||||
|
val mangaList = mangaListDto.data.map { mangaDataDto ->
|
||||||
|
helper.createManga(
|
||||||
|
mangaDataDto,
|
||||||
|
extLang,
|
||||||
|
preferences.coverQuality,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return MangasPage(mangaList, mangaListDto.meta.hasNextPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search manga section
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
if (query.startsWith(NamiComiConstants.prefixIdSearch)) {
|
||||||
|
val mangaId = query.removePrefix(NamiComiConstants.prefixIdSearch)
|
||||||
|
|
||||||
|
if (mangaId.isEmpty()) {
|
||||||
|
throw Exception(helper.intl["invalid_manga_id"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the query is an ID, return the manga directly
|
||||||
|
val url = NamiComiConstants.apiSearchUrl.toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("ids[]", query.removePrefix(NamiComiConstants.prefixIdSearch))
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.coverArt)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tempUrl = NamiComiConstants.apiSearchUrl.toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("limit", NamiComiConstants.mangaLimit.toString())
|
||||||
|
.addQueryParameter("offset", helper.getMangaListOffset(page))
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.coverArt)
|
||||||
|
|
||||||
|
val actualQuery = query.replace(NamiComiConstants.whitespaceRegex, " ")
|
||||||
|
if (actualQuery.isNotBlank()) {
|
||||||
|
tempUrl.addQueryParameter("title", actualQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalUrl = helper.filters.addFiltersToUrl(
|
||||||
|
url = tempUrl,
|
||||||
|
filters = filters.ifEmpty { getFilterList() },
|
||||||
|
extLang = extLang,
|
||||||
|
)
|
||||||
|
|
||||||
|
return GET(finalUrl, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||||
|
|
||||||
|
// Manga Details section
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String =
|
||||||
|
"$baseUrl/$extLang/title/${manga.url}/${helper.titleToSlug(manga.title)}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the API endpoint URL for the entry details.
|
||||||
|
*/
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||||
|
val url = (NamiComiConstants.apiMangaUrl + manga.url).toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.coverArt)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.organization)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.tag)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.primaryTag)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.secondaryTag)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val manga = response.parseAs<MangaDto>()
|
||||||
|
|
||||||
|
return helper.createManga(
|
||||||
|
manga.data!!,
|
||||||
|
extLang,
|
||||||
|
preferences.coverQuality,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chapter list section
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the API endpoint URL for the first page of chapter list.
|
||||||
|
*/
|
||||||
|
override fun chapterListRequest(manga: SManga): Request {
|
||||||
|
return paginatedChapterListRequest(manga.url, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required because the chapter list API endpoint is paginated.
|
||||||
|
*/
|
||||||
|
private fun paginatedChapterListRequest(mangaId: String, offset: Int): Request {
|
||||||
|
val url = NamiComiConstants.apiChapterUrl.toHttpUrl().newBuilder()
|
||||||
|
.addQueryParameter("titleId", mangaId)
|
||||||
|
.addQueryParameter("includes[]", NamiComiConstants.organization)
|
||||||
|
.addQueryParameter("limit", "500")
|
||||||
|
.addQueryParameter("offset", offset.toString())
|
||||||
|
.addQueryParameter("translatedLanguages[]", extLang)
|
||||||
|
.addQueryParameter("order[volume]", "desc")
|
||||||
|
.addQueryParameter("order[chapter]", "desc")
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests information about gated chapters (requiring payment & login).
|
||||||
|
*/
|
||||||
|
private fun accessibleChapterListRequest(chapterIds: List<String>): Request {
|
||||||
|
return POST(
|
||||||
|
NamiComiConstants.apiGatingCheckUrl,
|
||||||
|
headers,
|
||||||
|
chapterIds
|
||||||
|
.map { EntityAccessRequestItemDto(it, NamiComiConstants.chapter) }
|
||||||
|
.let { helper.json.encodeToString(EntityAccessRequestDto(it)) }
|
||||||
|
.toRequestBody(),
|
||||||
|
CacheControl.FORCE_NETWORK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
if (response.code == 204) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaId = response.request.url.toString()
|
||||||
|
.substringBefore("/chapter")
|
||||||
|
.substringAfter("${NamiComiConstants.apiMangaUrl}/")
|
||||||
|
|
||||||
|
val chapterListResponse = response.parseAs<ChapterListDto>()
|
||||||
|
val chapterListResults = chapterListResponse.data.toMutableList()
|
||||||
|
var offset = chapterListResponse.meta.offset
|
||||||
|
var hasNextPage = chapterListResponse.meta.hasNextPage
|
||||||
|
|
||||||
|
// Max results that can be returned is 500 so need to make more API
|
||||||
|
// calls if the chapter list response has a next page.
|
||||||
|
while (hasNextPage) {
|
||||||
|
offset += chapterListResponse.meta.limit
|
||||||
|
|
||||||
|
val newRequest = paginatedChapterListRequest(mangaId, offset)
|
||||||
|
val newResponse = client.newCall(newRequest).execute()
|
||||||
|
val newChapterList = newResponse.parseAs<ChapterListDto>()
|
||||||
|
chapterListResults.addAll(newChapterList.data)
|
||||||
|
|
||||||
|
hasNextPage = newChapterList.meta.hasNextPage
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no chapters, don't attempt to check gating
|
||||||
|
if (chapterListResults.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val gatingCheckRequest = accessibleChapterListRequest(chapterListResults.map { it.id })
|
||||||
|
val gatingCheckResponse = client.newCall(gatingCheckRequest).execute()
|
||||||
|
val accessibleChapterMap = gatingCheckResponse.parseAs<EntityAccessMapDto>()
|
||||||
|
.data?.attributes?.map ?: emptyMap()
|
||||||
|
|
||||||
|
return chapterListResults.mapNotNull {
|
||||||
|
val isAccessible = accessibleChapterMap[it.id]!!
|
||||||
|
when {
|
||||||
|
// Chapter can be viewed
|
||||||
|
isAccessible -> helper.createChapter(it)
|
||||||
|
// Chapter cannot be viewed and user wants to see locked chapters
|
||||||
|
preferences.showLockedChapters -> {
|
||||||
|
helper.createChapter(it).apply {
|
||||||
|
name = "${NamiComiConstants.lockSymbol} $name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Ignore locked chapters otherwise
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter): String =
|
||||||
|
"$baseUrl/$extLang/chapter/${chapter.url}"
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val chapterId = chapter.url
|
||||||
|
val url = "${NamiComiConstants.apiUrl}/images/chapter/$chapterId?newQualities=true"
|
||||||
|
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val chapterId = response.request.url.pathSegments.last()
|
||||||
|
val pageListDataDto = response.parseAs<PageListDto>().data ?: return emptyList()
|
||||||
|
|
||||||
|
val hash = pageListDataDto.hash
|
||||||
|
val prefix = "${pageListDataDto.baseUrl}/chapter/$chapterId/$hash"
|
||||||
|
|
||||||
|
val urls = if (preferences.useDataSaver) {
|
||||||
|
pageListDataDto.low.map { prefix + "/low/${it.filename}" }
|
||||||
|
} else {
|
||||||
|
pageListDataDto.source.map { prefix + "/source/${it.filename}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return urls.mapIndexed { index, url ->
|
||||||
|
Page(index, url, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String = ""
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
val coverQualityPref = ListPreference(screen.context).apply {
|
||||||
|
key = NamiComiConstants.getCoverQualityPreferenceKey(extLang)
|
||||||
|
title = helper.intl["cover_quality"]
|
||||||
|
entries = NamiComiConstants.getCoverQualityPreferenceEntries(helper.intl)
|
||||||
|
entryValues = NamiComiConstants.getCoverQualityPreferenceEntryValues()
|
||||||
|
setDefaultValue(NamiComiConstants.getCoverQualityPreferenceDefaultValue())
|
||||||
|
summary = "%s"
|
||||||
|
}
|
||||||
|
|
||||||
|
val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = NamiComiConstants.getDataSaverPreferenceKey(extLang)
|
||||||
|
title = helper.intl["data_saver"]
|
||||||
|
summary = helper.intl["data_saver_summary"]
|
||||||
|
setDefaultValue(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val showLockedChaptersPref = SwitchPreferenceCompat(screen.context).apply {
|
||||||
|
key = NamiComiConstants.getShowLockedChaptersPreferenceKey(extLang)
|
||||||
|
title = helper.intl["show_locked_chapters"]
|
||||||
|
summary = helper.intl["show_locked_chapters_summary"]
|
||||||
|
setDefaultValue(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
screen.addPreference(coverQualityPref)
|
||||||
|
screen.addPreference(dataSaverPref)
|
||||||
|
screen.addPreference(showLockedChaptersPref)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList =
|
||||||
|
helper.filters.getFilterList(helper.intl)
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T = use {
|
||||||
|
helper.json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SharedPreferences.coverQuality
|
||||||
|
get() = getString(NamiComiConstants.getCoverQualityPreferenceKey(extLang), "")
|
||||||
|
|
||||||
|
private val SharedPreferences.useDataSaver
|
||||||
|
get() = getBoolean(NamiComiConstants.getDataSaverPreferenceKey(extLang), false)
|
||||||
|
|
||||||
|
private val SharedPreferences.showLockedChapters
|
||||||
|
get() = getBoolean(NamiComiConstants.getShowLockedChaptersPreferenceKey(extLang), false)
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
object NamiComiConstants {
|
||||||
|
const val mangaLimit = 20
|
||||||
|
|
||||||
|
val whitespaceRegex = "\\s".toRegex()
|
||||||
|
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
|
||||||
|
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||||
|
|
||||||
|
const val lockSymbol = "🔒"
|
||||||
|
|
||||||
|
// Language codes used for translations
|
||||||
|
const val english = "en"
|
||||||
|
|
||||||
|
// JSON discriminators
|
||||||
|
const val chapter = "chapter"
|
||||||
|
const val manga = "title"
|
||||||
|
const val coverArt = "cover_art"
|
||||||
|
const val organization = "organization"
|
||||||
|
const val tag = "tag"
|
||||||
|
const val primaryTag = "primary_tag"
|
||||||
|
const val secondaryTag = "secondary_tag"
|
||||||
|
const val imageData = "image_data"
|
||||||
|
const val entityAccessMap = "entity_access_map"
|
||||||
|
|
||||||
|
// URLs & API endpoints
|
||||||
|
const val webUrl = "https://namicomi.com"
|
||||||
|
const val cdnUrl = "https://uploads.namicomi.com"
|
||||||
|
const val apiUrl = "https://api.namicomi.com"
|
||||||
|
const val apiMangaUrl = "$apiUrl/title"
|
||||||
|
const val apiSearchUrl = "$apiMangaUrl/search"
|
||||||
|
const val apiChapterUrl = "$apiUrl/chapter"
|
||||||
|
const val apiGatingCheckUrl = "$apiUrl/gating/check"
|
||||||
|
|
||||||
|
// Search prefix for title ids
|
||||||
|
const val prefixIdSearch = "id:"
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
private const val coverQualityPref = "thumbnailQuality"
|
||||||
|
fun getCoverQualityPreferenceKey(extLang: String): String = "${coverQualityPref}_$extLang"
|
||||||
|
fun getCoverQualityPreferenceEntries(intl: Intl) =
|
||||||
|
arrayOf(intl["cover_quality_original"], intl["cover_quality_medium"], intl["cover_quality_low"])
|
||||||
|
fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg")
|
||||||
|
fun getCoverQualityPreferenceDefaultValue() = getCoverQualityPreferenceEntryValues()[0]
|
||||||
|
|
||||||
|
private const val dataSaverPref = "dataSaver"
|
||||||
|
fun getDataSaverPreferenceKey(extLang: String): String = "${dataSaverPref}_$extLang"
|
||||||
|
|
||||||
|
private const val showLockedChaptersPref = "showLockedChapters"
|
||||||
|
fun getShowLockedChaptersPreferenceKey(extLang: String): String = "${showLockedChaptersPref}_$extLang"
|
||||||
|
|
||||||
|
// Tag types
|
||||||
|
private const val tagGroupContent = "content-warnings"
|
||||||
|
private const val tagGroupFormat = "format"
|
||||||
|
private const val tagGroupGenre = "genre"
|
||||||
|
private const val tagGroupTheme = "theme"
|
||||||
|
val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
|
class NamiComiFactory : SourceFactory {
|
||||||
|
override fun createSources(): List<Source> = listOf(
|
||||||
|
NamiComiEnglish(),
|
||||||
|
NamiComiArabic(),
|
||||||
|
NamiComiBulgarian(),
|
||||||
|
NamiComiCatalan(),
|
||||||
|
NamiComiChineseSimplified(),
|
||||||
|
NamiComiChineseTraditional(),
|
||||||
|
NamiComiCroatian(),
|
||||||
|
NamiComiCzech(),
|
||||||
|
NamiComiDanish(),
|
||||||
|
NamiComiDutch(),
|
||||||
|
NamiComiEstonian(),
|
||||||
|
NamiComiFilipino(),
|
||||||
|
NamiComiFinnish(),
|
||||||
|
NamiComiFrench(),
|
||||||
|
NamiComiGerman(),
|
||||||
|
NamiComiGreek(),
|
||||||
|
NamiComiHebrew(),
|
||||||
|
NamiComiHindi(),
|
||||||
|
NamiComiHungarian(),
|
||||||
|
NamiComiIcelandic(),
|
||||||
|
NamiComiIrish(),
|
||||||
|
NamiComiIndonesian(),
|
||||||
|
NamiComiItalian(),
|
||||||
|
NamiComiJapanese(),
|
||||||
|
NamiComiKorean(),
|
||||||
|
NamiComiLithuanian(),
|
||||||
|
NamiComiMalay(),
|
||||||
|
NamiComiNepali(),
|
||||||
|
NamiComiNorwegian(),
|
||||||
|
NamiComiPanjabi(),
|
||||||
|
NamiComiPersian(),
|
||||||
|
NamiComiPolish(),
|
||||||
|
NamiComiPortugueseBrazil(),
|
||||||
|
NamiComiPortuguesePortugal(),
|
||||||
|
NamiComiRussian(),
|
||||||
|
NamiComiSlovak(),
|
||||||
|
NamiComiSlovenian(),
|
||||||
|
NamiComiSpanishLatinAmerica(),
|
||||||
|
NamiComiSpanishSpain(),
|
||||||
|
NamiComiSwedish(),
|
||||||
|
NamiComiThai(),
|
||||||
|
NamiComiTurkish(),
|
||||||
|
NamiComiUkrainian(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class NamiComiArabic : NamiComi("ar")
|
||||||
|
class NamiComiBulgarian : NamiComi("bg")
|
||||||
|
class NamiComiCatalan : NamiComi("ca")
|
||||||
|
class NamiComiChineseSimplified : NamiComi("zh-Hans", "zh-hans")
|
||||||
|
class NamiComiChineseTraditional : NamiComi("zh-Hant", "zh-hant")
|
||||||
|
class NamiComiCroatian : NamiComi("hr")
|
||||||
|
class NamiComiCzech : NamiComi("cs")
|
||||||
|
class NamiComiDanish : NamiComi("da")
|
||||||
|
class NamiComiDutch : NamiComi("nl")
|
||||||
|
class NamiComiEnglish : NamiComi("en")
|
||||||
|
class NamiComiEstonian : NamiComi("et")
|
||||||
|
class NamiComiFilipino : NamiComi("fil")
|
||||||
|
class NamiComiFinnish : NamiComi("fi")
|
||||||
|
class NamiComiFrench : NamiComi("fr")
|
||||||
|
class NamiComiGerman : NamiComi("de")
|
||||||
|
class NamiComiGreek : NamiComi("el")
|
||||||
|
class NamiComiHebrew : NamiComi("he")
|
||||||
|
class NamiComiHindi : NamiComi("hi")
|
||||||
|
class NamiComiHungarian : NamiComi("hu")
|
||||||
|
class NamiComiIcelandic : NamiComi("is")
|
||||||
|
class NamiComiIrish : NamiComi("ga")
|
||||||
|
class NamiComiIndonesian : NamiComi("id")
|
||||||
|
class NamiComiItalian : NamiComi("it")
|
||||||
|
class NamiComiJapanese : NamiComi("ja")
|
||||||
|
class NamiComiKorean : NamiComi("ko")
|
||||||
|
class NamiComiLithuanian : NamiComi("lt")
|
||||||
|
class NamiComiMalay : NamiComi("ms")
|
||||||
|
class NamiComiNepali : NamiComi("ne")
|
||||||
|
class NamiComiNorwegian : NamiComi("no")
|
||||||
|
class NamiComiPanjabi : NamiComi("pa")
|
||||||
|
class NamiComiPersian : NamiComi("fa")
|
||||||
|
class NamiComiPolish : NamiComi("pl")
|
||||||
|
class NamiComiPortugueseBrazil : NamiComi("pt-BR", "pt-br")
|
||||||
|
class NamiComiPortuguesePortugal : NamiComi("pt", "pt-pt")
|
||||||
|
class NamiComiRussian : NamiComi("ru")
|
||||||
|
class NamiComiSlovak : NamiComi("sk")
|
||||||
|
class NamiComiSlovenian : NamiComi("sl")
|
||||||
|
class NamiComiSpanishLatinAmerica : NamiComi("es-419")
|
||||||
|
class NamiComiSpanishSpain : NamiComi("es", "es-es")
|
||||||
|
class NamiComiSwedish : NamiComi("sv")
|
||||||
|
class NamiComiThai : NamiComi("th")
|
||||||
|
class NamiComiTurkish : NamiComi("tr")
|
||||||
|
class NamiComiUkrainian : NamiComi("uk")
|
|
@ -0,0 +1,289 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ContentRatingDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.StatusDto
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import okhttp3.HttpUrl
|
||||||
|
|
||||||
|
class NamiComiFilters {
|
||||||
|
|
||||||
|
internal fun getFilterList(intl: Intl): FilterList = FilterList(
|
||||||
|
HasAvailableChaptersFilter(intl),
|
||||||
|
ContentRatingList(intl, getContentRatings(intl)),
|
||||||
|
StatusList(intl, getStatus(intl)),
|
||||||
|
SortFilter(intl, getSortables(intl)),
|
||||||
|
TagsFilter(intl, getTagFilters(intl)),
|
||||||
|
TagList(intl["content"], getContents(intl)),
|
||||||
|
TagList(intl["format"], getFormats(intl)),
|
||||||
|
TagList(intl["genre"], getGenres(intl)),
|
||||||
|
TagList(intl["theme"], getThemes(intl)),
|
||||||
|
)
|
||||||
|
|
||||||
|
private interface UrlQueryFilter {
|
||||||
|
fun addQueryParameter(url: HttpUrl.Builder, extLang: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class HasAvailableChaptersFilter(intl: Intl) :
|
||||||
|
Filter.CheckBox(intl["has_available_chapters"]),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
if (state) {
|
||||||
|
url.addQueryParameter("hasAvailableChapters", "true")
|
||||||
|
url.addQueryParameter("availableTranslatedLanguages[]", extLang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ContentRating(name: String, val value: String) : Filter.CheckBox(name)
|
||||||
|
private class ContentRatingList(intl: Intl, contentRating: List<ContentRating>) :
|
||||||
|
Filter.Group<ContentRating>(intl["content_rating"], contentRating),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
state.filter(ContentRating::state)
|
||||||
|
.forEach { url.addQueryParameter("contentRatings[]", it.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getContentRatings(intl: Intl) = listOf(
|
||||||
|
ContentRating(intl["content_rating_safe"], ContentRatingDto.SAFE.value),
|
||||||
|
ContentRating(intl["content_rating_restricted"], ContentRatingDto.RESTRICTED.value),
|
||||||
|
ContentRating(intl["content_rating_mature"], ContentRatingDto.MATURE.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class Status(name: String, val value: String) : Filter.CheckBox(name)
|
||||||
|
private class StatusList(intl: Intl, status: List<Status>) :
|
||||||
|
Filter.Group<Status>(intl["status"], status),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
state.filter(Status::state)
|
||||||
|
.forEach { url.addQueryParameter("publicationStatuses[]", it.value) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getStatus(intl: Intl) = listOf(
|
||||||
|
Status(intl["status_ongoing"], StatusDto.ONGOING.value),
|
||||||
|
Status(intl["status_completed"], StatusDto.COMPLETED.value),
|
||||||
|
Status(intl["status_hiatus"], StatusDto.HIATUS.value),
|
||||||
|
Status(intl["status_cancelled"], StatusDto.CANCELLED.value),
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Sortable(val title: String, val value: String) {
|
||||||
|
override fun toString(): String = title
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSortables(intl: Intl) = arrayOf(
|
||||||
|
Sortable(intl["sort_alphabetic"], "title"),
|
||||||
|
Sortable(intl["sort_number_of_chapters"], "chapterCount"),
|
||||||
|
Sortable(intl["sort_number_of_follows"], "followCount"),
|
||||||
|
Sortable(intl["sort_number_of_likes"], "reactions"),
|
||||||
|
Sortable(intl["sort_number_of_comments"], "commentCount"),
|
||||||
|
Sortable(intl["sort_content_created_at"], "publishedAt"),
|
||||||
|
Sortable(intl["sort_views"], "views"),
|
||||||
|
Sortable(intl["sort_year"], "year"),
|
||||||
|
Sortable(intl["sort_rating"], "rating"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter(intl: Intl, private val sortables: Array<Sortable>) :
|
||||||
|
Filter.Sort(
|
||||||
|
intl["sort"],
|
||||||
|
sortables.map(Sortable::title).toTypedArray(),
|
||||||
|
Selection(5, false),
|
||||||
|
),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
if (state != null) {
|
||||||
|
val query = sortables[state!!.index].value
|
||||||
|
val value = if (state!!.ascending) "asc" else "desc"
|
||||||
|
|
||||||
|
url.addQueryParameter("order[$query]", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Tag(val id: String, name: String) : Filter.TriState(name)
|
||||||
|
|
||||||
|
private class TagList(collection: String, tags: List<Tag>) :
|
||||||
|
Filter.Group<Tag>(collection, tags),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
state.forEach { tag ->
|
||||||
|
if (tag.isIncluded()) {
|
||||||
|
url.addQueryParameter("includedTags[]", tag.id)
|
||||||
|
} else if (tag.isExcluded()) {
|
||||||
|
url.addQueryParameter("excludedTags[]", tag.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getContents(intl: Intl): List<Tag> {
|
||||||
|
val tags = listOf(
|
||||||
|
Tag("drugs", intl["content_warnings_drugs"]),
|
||||||
|
Tag("gambling", intl["content_warnings_gambling"]),
|
||||||
|
Tag("gore", intl["content_warnings_gore"]),
|
||||||
|
Tag("mental-disorders", intl["content_warnings_mental_disorders"]),
|
||||||
|
Tag("physical-abuse", intl["content_warnings_physical_abuse"]),
|
||||||
|
Tag("racism", intl["content_warnings_racism"]),
|
||||||
|
Tag("self-harm", intl["content_warnings_self_harm"]),
|
||||||
|
Tag("sexual-abuse", intl["content_warnings_sexual_abuse"]),
|
||||||
|
Tag("verbal-abuse", intl["content_warnings_verbal_abuse"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return tags.sortIfTranslated(intl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFormats(intl: Intl): List<Tag> {
|
||||||
|
val tags = listOf(
|
||||||
|
Tag("4-koma", intl["format_4_koma"]),
|
||||||
|
Tag("adaptation", intl["format_adaptation"]),
|
||||||
|
Tag("anthology", intl["format_anthology"]),
|
||||||
|
Tag("full-color", intl["format_full_color"]),
|
||||||
|
Tag("oneshot", intl["format_oneshot"]),
|
||||||
|
Tag("silent", intl["format_silent"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return tags.sortIfTranslated(intl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGenres(intl: Intl): List<Tag> {
|
||||||
|
val tags = listOf(
|
||||||
|
Tag("action", intl["genre_action"]),
|
||||||
|
Tag("adventure", intl["genre_adventure"]),
|
||||||
|
Tag("boys-love", intl["genre_boys_love"]),
|
||||||
|
Tag("comedy", intl["genre_comedy"]),
|
||||||
|
Tag("crime", intl["genre_crime"]),
|
||||||
|
Tag("drama", intl["genre_drama"]),
|
||||||
|
Tag("fantasy", intl["genre_fantasy"]),
|
||||||
|
Tag("girls-love", intl["genre_girls_love"]),
|
||||||
|
Tag("historical", intl["genre_historical"]),
|
||||||
|
Tag("horror", intl["genre_horror"]),
|
||||||
|
Tag("isekai", intl["genre_isekai"]),
|
||||||
|
Tag("mecha", intl["genre_mecha"]),
|
||||||
|
Tag("medical", intl["genre_medical"]),
|
||||||
|
Tag("mystery", intl["genre_mystery"]),
|
||||||
|
Tag("philosophical", intl["genre_philosophical"]),
|
||||||
|
Tag("psychological", intl["genre_psychological"]),
|
||||||
|
Tag("romance", intl["genre_romance"]),
|
||||||
|
Tag("sci-fi", intl["genre_sci_fi"]),
|
||||||
|
Tag("slice-of-life", intl["genre_slice_of_life"]),
|
||||||
|
Tag("sports", intl["genre_sports"]),
|
||||||
|
Tag("superhero", intl["genre_superhero"]),
|
||||||
|
Tag("thriller", intl["genre_thriller"]),
|
||||||
|
Tag("tragedy", intl["genre_tragedy"]),
|
||||||
|
Tag("wuxia", intl["genre_wuxia"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return tags.sortIfTranslated(intl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getThemes(intl: Intl): List<Tag> {
|
||||||
|
val tags = listOf(
|
||||||
|
Tag("aliens", intl["theme_aliens"]),
|
||||||
|
Tag("animals", intl["theme_animals"]),
|
||||||
|
Tag("cooking", intl["theme_cooking"]),
|
||||||
|
Tag("crossdressing", intl["theme_crossdressing"]),
|
||||||
|
Tag("delinquents", intl["theme_delinquents"]),
|
||||||
|
Tag("demons", intl["theme_demons"]),
|
||||||
|
Tag("genderswap", intl["theme_genderswap"]),
|
||||||
|
Tag("ghosts", intl["theme_ghosts"]),
|
||||||
|
Tag("gyaru", intl["theme_gyaru"]),
|
||||||
|
Tag("harem", intl["theme_harem"]),
|
||||||
|
Tag("mafia", intl["theme_mafia"]),
|
||||||
|
Tag("magic", intl["theme_magic"]),
|
||||||
|
Tag("magical-girls", intl["theme_magical_girls"]),
|
||||||
|
Tag("martial-arts", intl["theme_martial_arts"]),
|
||||||
|
Tag("military", intl["theme_military"]),
|
||||||
|
Tag("monster-girls", intl["theme_monster_girls"]),
|
||||||
|
Tag("monsters", intl["theme_monsters"]),
|
||||||
|
Tag("music", intl["theme_music"]),
|
||||||
|
Tag("ninja", intl["theme_ninja"]),
|
||||||
|
Tag("office-workers", intl["theme_office_workers"]),
|
||||||
|
Tag("police", intl["theme_police"]),
|
||||||
|
Tag("post-apocalyptic", intl["theme_post_apocalyptic"]),
|
||||||
|
Tag("reincarnation", intl["theme_reincarnation"]),
|
||||||
|
Tag("reverse-harem", intl["theme_reverse_harem"]),
|
||||||
|
Tag("samurai", intl["theme_samurai"]),
|
||||||
|
Tag("school-life", intl["theme_school_life"]),
|
||||||
|
Tag("supernatural", intl["theme_supernatural"]),
|
||||||
|
Tag("survival", intl["theme_survival"]),
|
||||||
|
Tag("time-travel", intl["theme_time_travel"]),
|
||||||
|
Tag("traditional-games", intl["theme_traditional_games"]),
|
||||||
|
Tag("vampires", intl["theme_vampires"]),
|
||||||
|
Tag("video-games", intl["theme_video_games"]),
|
||||||
|
Tag("villainess", intl["theme_villainess"]),
|
||||||
|
Tag("virtual-reality", intl["theme_virtual_reality"]),
|
||||||
|
Tag("zombies", intl["theme_zombies"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
return tags.sortIfTranslated(intl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags taken from: https://api.namicomi.com/title/tags
|
||||||
|
internal fun getTags(intl: Intl): List<Tag> {
|
||||||
|
return getContents(intl) + getFormats(intl) + getGenres(intl) + getThemes(intl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class TagMode(val title: String, val value: String) {
|
||||||
|
override fun toString(): String = title
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTagModes(intl: Intl) = arrayOf(
|
||||||
|
TagMode(intl["mode_and"], "and"),
|
||||||
|
TagMode(intl["mode_or"], "or"),
|
||||||
|
)
|
||||||
|
|
||||||
|
private class TagInclusionMode(intl: Intl, modes: Array<TagMode>) :
|
||||||
|
Filter.Select<TagMode>(intl["included_tags_mode"], modes, 0),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
url.addQueryParameter("includedTagsMode", values[state].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TagExclusionMode(intl: Intl, modes: Array<TagMode>) :
|
||||||
|
Filter.Select<TagMode>(intl["excluded_tags_mode"], modes, 1),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
url.addQueryParameter("excludedTagsMode", values[state].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TagsFilter(intl: Intl, innerFilters: FilterList) :
|
||||||
|
Filter.Group<Filter<*>>(intl["tags_mode"], innerFilters),
|
||||||
|
UrlQueryFilter {
|
||||||
|
|
||||||
|
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||||
|
state.filterIsInstance<UrlQueryFilter>()
|
||||||
|
.forEach { filter -> filter.addQueryParameter(url, extLang) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTagFilters(intl: Intl): FilterList = FilterList(
|
||||||
|
TagInclusionMode(intl, getTagModes(intl)),
|
||||||
|
TagExclusionMode(intl, getTagModes(intl)),
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, extLang: String): HttpUrl {
|
||||||
|
filters.filterIsInstance<UrlQueryFilter>()
|
||||||
|
.forEach { filter -> filter.addQueryParameter(url, extLang) }
|
||||||
|
|
||||||
|
return url.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Tag>.sortIfTranslated(intl: Intl): List<Tag> = apply {
|
||||||
|
if (intl.chosenLanguage == NamiComiConstants.english) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedWith(compareBy(intl.collator, Tag::name))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.AbstractTagDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ChapterDataDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ContentRatingDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.CoverArtDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.MangaDataDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.OrganizationDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.StatusDto
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.dto.UnknownEntity
|
||||||
|
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.plus
|
||||||
|
import kotlinx.serialization.modules.polymorphic
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class NamiComiHelper(lang: String) {
|
||||||
|
|
||||||
|
val filters = NamiComiFilters()
|
||||||
|
|
||||||
|
val json = Json {
|
||||||
|
isLenient = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
serializersModule += SerializersModule {
|
||||||
|
polymorphic(EntityDto::class) {
|
||||||
|
defaultDeserializer { UnknownEntity.serializer() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val intl = Intl(
|
||||||
|
language = lang,
|
||||||
|
baseLanguage = NamiComiConstants.english,
|
||||||
|
availableLanguages = setOf(NamiComiConstants.english),
|
||||||
|
classLoader = this::class.java.classLoader!!,
|
||||||
|
createMessageFileName = { lang -> Intl.createDefaultMessageFileName(lang) },
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the manga offset pages are 1 based, so subtract 1
|
||||||
|
*/
|
||||||
|
fun getMangaListOffset(page: Int): String = (NamiComiConstants.mangaLimit * (page - 1)).toString()
|
||||||
|
|
||||||
|
private fun getPublicationStatus(mangaDataDto: MangaDataDto): Int {
|
||||||
|
return when (mangaDataDto.attributes!!.publicationStatus) {
|
||||||
|
StatusDto.ONGOING -> SManga.ONGOING
|
||||||
|
StatusDto.CANCELLED -> SManga.CANCELLED
|
||||||
|
StatusDto.COMPLETED -> SManga.COMPLETED
|
||||||
|
StatusDto.HIATUS -> SManga.ON_HIATUS
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseDate(dateAsString: String): Long =
|
||||||
|
NamiComiConstants.dateFormatter.parse(dateAsString)?.time ?: 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an [SManga] from the JSON element with all attributes filled.
|
||||||
|
*/
|
||||||
|
fun createManga(
|
||||||
|
mangaDataDto: MangaDataDto,
|
||||||
|
lang: String,
|
||||||
|
coverSuffix: String?,
|
||||||
|
): SManga {
|
||||||
|
val attr = mangaDataDto.attributes!!
|
||||||
|
|
||||||
|
// Things that will go with the genre tags but aren't actually genre
|
||||||
|
val extLocale = Locale.forLanguageTag(lang)
|
||||||
|
|
||||||
|
val nonGenres = listOfNotNull(
|
||||||
|
attr.contentRating
|
||||||
|
.takeIf { it != ContentRatingDto.SAFE }
|
||||||
|
?.let { intl.format("content_rating_genre", intl["content_rating_${it.name.lowercase()}"]) },
|
||||||
|
attr.originalLanguage
|
||||||
|
?.let { Locale.forLanguageTag(it) }
|
||||||
|
?.getDisplayName(extLocale)
|
||||||
|
?.replaceFirstChar { it.uppercase(extLocale) },
|
||||||
|
)
|
||||||
|
|
||||||
|
val organization = mangaDataDto.relationships
|
||||||
|
.filterIsInstance<OrganizationDto>()
|
||||||
|
.mapNotNull { it.attributes?.name }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
val coverFileName = mangaDataDto.relationships
|
||||||
|
.filterIsInstance<CoverArtDto>()
|
||||||
|
.firstOrNull()
|
||||||
|
?.attributes?.fileName
|
||||||
|
|
||||||
|
val tags = filters.getTags(intl).associate { it.id to it.name }
|
||||||
|
|
||||||
|
val genresMap = mangaDataDto.relationships
|
||||||
|
.filterIsInstance<AbstractTagDto>()
|
||||||
|
.groupBy({ it.attributes!!.group }) { tagDto -> tags[tagDto.id] }
|
||||||
|
.mapValues { it.value.filterNotNull().sortedWith(intl.collator) }
|
||||||
|
|
||||||
|
val genreList = NamiComiConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
|
||||||
|
|
||||||
|
val desc = (attr.description[lang] ?: attr.description["en"])
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
|
return SManga.create().apply {
|
||||||
|
initialized = true
|
||||||
|
url = mangaDataDto.id
|
||||||
|
description = desc
|
||||||
|
author = organization.joinToString()
|
||||||
|
status = getPublicationStatus(mangaDataDto)
|
||||||
|
genre = genreList
|
||||||
|
.filter(String::isNotEmpty)
|
||||||
|
.joinToString()
|
||||||
|
|
||||||
|
mangaDataDto.attributes.title.let { titleMap ->
|
||||||
|
title = titleMap[lang] ?: titleMap.values.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
coverFileName?.let {
|
||||||
|
thumbnail_url = when (!coverSuffix.isNullOrEmpty()) {
|
||||||
|
true -> "${NamiComiConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName$coverSuffix"
|
||||||
|
else -> "${NamiComiConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the [SChapter] from the JSON element.
|
||||||
|
*/
|
||||||
|
fun createChapter(chapterDataDto: ChapterDataDto): SChapter {
|
||||||
|
val attr = chapterDataDto.attributes!!
|
||||||
|
val chapterName = mutableListOf<String>()
|
||||||
|
|
||||||
|
attr.volume?.let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
chapterName.add("Vol.$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attr.chapter?.let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
chapterName.add("Ch.$it")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attr.name?.let {
|
||||||
|
if (it.isNotEmpty()) {
|
||||||
|
if (chapterName.isNotEmpty()) {
|
||||||
|
chapterName.add("-")
|
||||||
|
}
|
||||||
|
chapterName.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SChapter.create().apply {
|
||||||
|
url = chapterDataDto.id
|
||||||
|
name = chapterName.joinToString(" ")
|
||||||
|
date_upload = parseDate(attr.publishAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun titleToSlug(title: String) = title.trim()
|
||||||
|
.lowercase(Locale.US)
|
||||||
|
.replace(titleSpecialCharactersRegex, "-")
|
||||||
|
.replace(trailingHyphenRegex, "")
|
||||||
|
.split("-")
|
||||||
|
.reduce { accumulator, element ->
|
||||||
|
val currentSlug = "$accumulator-$element"
|
||||||
|
if (currentSlug.length > 100) {
|
||||||
|
accumulator
|
||||||
|
} else {
|
||||||
|
currentSlug
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
|
||||||
|
val trailingHyphenRegex = "-+$".toRegex()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||||
|
|
||||||
|
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://namicomi.com/xx/title/yyy 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 NamiComiUrlActivity : Activity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val pathSegments = intent?.data?.pathSegments
|
||||||
|
|
||||||
|
// Supported path: /en/title/12345
|
||||||
|
if (pathSegments != null && pathSegments.size > 2) {
|
||||||
|
val titleId = pathSegments[2]
|
||||||
|
val mainIntent = Intent().apply {
|
||||||
|
action = "eu.kanade.tachiyomi.SEARCH"
|
||||||
|
putExtra("query", NamiComiConstants.prefixIdSearch + titleId)
|
||||||
|
putExtra("filter", packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startActivity(mainIntent)
|
||||||
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
Log.e("NamiComiUrlActivity", e.toString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Toast.makeText(this, "This URL cannot be handled by the Namicomi extension.", Toast.LENGTH_SHORT).show()
|
||||||
|
Log.e("NamiComiUrlActivity", "Could not parse URI from intent $intent")
|
||||||
|
}
|
||||||
|
|
||||||
|
finish()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
typealias ChapterListDto = PaginatedResponseDto<ChapterDataDto>
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.chapter)
|
||||||
|
class ChapterDataDto(override val attributes: ChapterAttributesDto? = null) : EntityDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ChapterAttributesDto(
|
||||||
|
val name: String?,
|
||||||
|
val volume: String?,
|
||||||
|
val chapter: String?,
|
||||||
|
val pages: Int,
|
||||||
|
val publishAt: String,
|
||||||
|
) : AttributesDto
|
|
@ -0,0 +1,15 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.coverArt)
|
||||||
|
class CoverArtDto(override val attributes: CoverArtAttributesDto? = null) : EntityDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class CoverArtAttributesDto(
|
||||||
|
val fileName: String? = null,
|
||||||
|
val locale: String? = null,
|
||||||
|
) : AttributesDto
|
|
@ -0,0 +1,30 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
typealias EntityAccessMapDto = ResponseDto<EntityAccessMapDataDto>
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.entityAccessMap)
|
||||||
|
class EntityAccessMapDataDto(
|
||||||
|
override val attributes: EntityAccessMapAttributesDto? = null,
|
||||||
|
) : EntityDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EntityAccessMapAttributesDto(
|
||||||
|
// Map of entity IDs to whether the user has access to them
|
||||||
|
val map: Map<String, Boolean>,
|
||||||
|
) : AttributesDto
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EntityAccessRequestDto(
|
||||||
|
val entities: List<EntityAccessRequestItemDto>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EntityAccessRequestItemDto(
|
||||||
|
val entityId: String,
|
||||||
|
val entityType: String,
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class EntityDto {
|
||||||
|
val id: String = ""
|
||||||
|
val relationships: List<EntityDto> = emptyList()
|
||||||
|
abstract val attributes: AttributesDto?
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed interface AttributesDto
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class UnknownEntity(override val attributes: AttributesDto? = null) : EntityDto()
|
|
@ -0,0 +1,70 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
typealias MangaListDto = PaginatedResponseDto<MangaDataDto>
|
||||||
|
|
||||||
|
typealias MangaDto = ResponseDto<MangaDataDto>
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.manga)
|
||||||
|
class MangaDataDto(override val attributes: MangaAttributesDto? = null) : EntityDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class MangaAttributesDto(
|
||||||
|
// Title and description are maps of language codes to localized strings
|
||||||
|
val title: Map<String, String>,
|
||||||
|
val description: Map<String, String>,
|
||||||
|
val slug: String,
|
||||||
|
val originalLanguage: String?,
|
||||||
|
val year: Int?,
|
||||||
|
val contentRating: ContentRatingDto? = null,
|
||||||
|
val publicationStatus: StatusDto? = null,
|
||||||
|
) : AttributesDto
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class ContentRatingDto(val value: String) {
|
||||||
|
@SerialName("safe")
|
||||||
|
SAFE("safe"),
|
||||||
|
|
||||||
|
@SerialName("restricted")
|
||||||
|
RESTRICTED("restricted"),
|
||||||
|
|
||||||
|
@SerialName("mature")
|
||||||
|
MATURE("mature"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class StatusDto(val value: String) {
|
||||||
|
@SerialName("ongoing")
|
||||||
|
ONGOING("ongoing"),
|
||||||
|
|
||||||
|
@SerialName("completed")
|
||||||
|
COMPLETED("completed"),
|
||||||
|
|
||||||
|
@SerialName("hiatus")
|
||||||
|
HIATUS("hiatus"),
|
||||||
|
|
||||||
|
@SerialName("cancelled")
|
||||||
|
CANCELLED("cancelled"),
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class AbstractTagDto(override val attributes: TagAttributesDto? = null) : EntityDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.tag)
|
||||||
|
class TagDto : AbstractTagDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.primaryTag)
|
||||||
|
class PrimaryTagDto : AbstractTagDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.secondaryTag)
|
||||||
|
class SecondaryTagDto : AbstractTagDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class TagAttributesDto(val group: String) : AttributesDto
|
|
@ -0,0 +1,12 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.organization)
|
||||||
|
class OrganizationDto(override val attributes: OrganizationAttributesDto? = null) : EntityDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class OrganizationAttributesDto(val name: String) : AttributesDto
|
|
@ -0,0 +1,26 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
typealias PageListDto = ResponseDto<PageListDataDto>
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName(NamiComiConstants.imageData)
|
||||||
|
class PageListDataDto(
|
||||||
|
override val attributes: AttributesDto? = null,
|
||||||
|
val baseUrl: String,
|
||||||
|
val hash: String,
|
||||||
|
val source: List<PageImageDto>,
|
||||||
|
val high: List<PageImageDto>,
|
||||||
|
val medium: List<PageImageDto>,
|
||||||
|
val low: List<PageImageDto>,
|
||||||
|
) : EntityDto()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PageImageDto(
|
||||||
|
val size: Int?,
|
||||||
|
val filename: String,
|
||||||
|
val resolution: String?,
|
||||||
|
)
|
|
@ -0,0 +1,27 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PaginatedResponseDto<T : EntityDto>(
|
||||||
|
val result: String,
|
||||||
|
val data: List<T> = emptyList(),
|
||||||
|
val meta: PaginationStateDto,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ResponseDto<T : EntityDto>(
|
||||||
|
val result: String,
|
||||||
|
val type: String,
|
||||||
|
val data: T? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class PaginationStateDto(
|
||||||
|
val limit: Int = 0,
|
||||||
|
val offset: Int = 0,
|
||||||
|
val total: Int = 0,
|
||||||
|
) {
|
||||||
|
val hasNextPage: Boolean
|
||||||
|
get() = limit + offset < total
|
||||||
|
}
|
Loading…
Reference in New Issue