diff --git a/src/en/manta/AndroidManifest.xml b/src/en/manta/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/en/manta/AndroidManifest.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest package="eu.kanade.tachiyomi.extension" /> diff --git a/src/en/manta/build.gradle b/src/en/manta/build.gradle new file mode 100644 index 000000000..bf78b241a --- /dev/null +++ b/src/en/manta/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'Manta Comics' + pkgNameSuffix = 'en.manta' + extClass = '.MantaComics' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/manta/res/mipmap-hdpi/ic_launcher.png b/src/en/manta/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..1df104fef Binary files /dev/null and b/src/en/manta/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/manta/res/mipmap-mdpi/ic_launcher.png b/src/en/manta/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..a17a00850 Binary files /dev/null and b/src/en/manta/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/manta/res/mipmap-xhdpi/ic_launcher.png b/src/en/manta/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..72b9579f3 Binary files /dev/null and b/src/en/manta/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/manta/res/mipmap-xxhdpi/ic_launcher.png b/src/en/manta/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..b2771784a Binary files /dev/null and b/src/en/manta/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/manta/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/manta/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..e05b2b87d Binary files /dev/null and b/src/en/manta/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/manta/res/web_hi_res_512.png b/src/en/manta/res/web_hi_res_512.png new file mode 100644 index 000000000..c3b3e63b5 Binary files /dev/null and b/src/en/manta/res/web_hi_res_512.png differ diff --git a/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaAPI.kt b/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaAPI.kt new file mode 100644 index 000000000..e8b8bb927 --- /dev/null +++ b/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaAPI.kt @@ -0,0 +1,127 @@ +package eu.kanade.tachiyomi.extension.en.manta + +import kotlinx.serialization.Serializable +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.concurrent.TimeUnit.MILLISECONDS as MS + +private val isoDate by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT) +} + +private inline val String?.timestamp: Long + get() = this?.substringBefore('.')?.let(isoDate::parse)?.time ?: 0L + +@Serializable +data class Series<T : Any>( + val data: T, + val id: Int, + val image: Cover, + val episodes: List<Episode>? = null +) { + override fun toString() = data.toString() +} + +@Serializable +data class Title(private val title: Name) { + override fun toString() = title.toString() +} + +@Serializable +data class Details( + val tags: List<Tag>, + val isCompleted: Boolean? = null, + private val description: Description, + private val creators: List<Creator> +) { + val artists by lazy { + creators.filter { it.role == "Illustration" } + } + + val authors by lazy { + creators.filter { it.role != "Illustration" }.ifEmpty { creators } + } + + override fun toString() = description.toString() +} + +@Serializable +data class Episode( + val id: Int, + val ord: Int, + val data: Data?, + private val createdAt: String, + val cutImages: List<Image>? = null +) { + val timestamp: Long + get() = createdAt.timestamp + + val isLocked: Boolean + get() = timeTillFree > 0 + + val waitingTime: String + get() = when (val days = MS.toDays(timeTillFree)) { + 0L -> "later today" + 1L -> "tomorrow" + else -> "in $days days" + } + + private val timeTillFree by lazy { + data?.freeAt.timestamp - System.currentTimeMillis() + } + + override fun toString() = buildString { + append(data?.title ?: "Episode $ord") + if (isLocked) append(" \uD83D\uDD12") + } +} + +@Serializable +data class Data( + val title: String? = null, + val freeAt: String? = null +) + +@Serializable +data class Creator( + private val name: String, + val role: String +) { + override fun toString() = name +} + +@Serializable +data class Description( + private val long: String, + private val short: String +) { + override fun toString() = "$short\n\n$long" +} + +@Serializable +data class Cover(private val `1280x1840_480`: Image) { + override fun toString() = `1280x1840_480`.toString() +} + +@Serializable +data class Image(private val downloadUrl: String) { + override fun toString() = downloadUrl +} + +@Serializable +data class Tag(private val name: Name) { + override fun toString() = name.toString() +} + +@Serializable +data class Name(private val en: String) { + override fun toString() = en +} + +@Serializable +data class Status( + private val description: String, + private val message: String +) { + override fun toString() = "$description: $message" +} diff --git a/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaComics.kt b/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaComics.kt new file mode 100644 index 000000000..83e1cb846 --- /dev/null +++ b/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaComics.kt @@ -0,0 +1,128 @@ +package eu.kanade.tachiyomi.extension.en.manta + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.asObservable +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.json.Json +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.injectLazy + +class MantaComics : HttpSource() { + override val name = "Manta" + + override val lang = "en" + + override val baseUrl = "https://manta.net" + + override val supportsLatest = false + + private val json by injectLazy<Json>() + + override fun headersBuilder() = super.headersBuilder() + .set("User-Agent", "Manta/167").set("Origin", baseUrl) + + override fun latestUpdatesRequest(page: Int) = + GET("$baseUrl/manta/v1/search/series?cat=New", headers) + + override fun fetchPopularManga(page: Int) = + latestUpdatesRequest(page).fetch(::searchMangaParse) + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = + filters.category.ifEmpty { if (query.isEmpty()) "New" else "" }.let { + GET("$baseUrl/manta/v1/search/series?cat=$it&q=$query", headers) + } + + override fun searchMangaParse(response: Response) = + response.parse<List<Series<Title>>>().map { + SManga.create().apply { + title = it.toString() + url = it.id.toString() + thumbnail_url = it.image.toString() + } + }.let { MangasPage(it, false) } + + override fun fetchSearchManga(page: Int, query: String, filters: FilterList) = + searchMangaRequest(page, query, filters).fetch(::searchMangaParse) + + // Request the actual manga URL for the webview + override fun mangaDetailsRequest(manga: SManga) = + GET("$baseUrl/series/${manga.url}") + + override fun mangaDetailsParse(response: Response) = + SManga.create().apply { + val data = response.parse<Series<Details>>().data + description = data.toString() + genre = data.tags.joinToString() + artist = data.artists.joinToString() + author = data.authors.joinToString() + status = when (data.isCompleted) { + true -> SManga.COMPLETED + else -> SManga.ONGOING + } + initialized = true + } + + override fun fetchMangaDetails(manga: SManga) = + chapterListRequest(manga).fetch(::mangaDetailsParse) + + override fun chapterListRequest(manga: SManga) = + GET("$baseUrl/front/v1/series/${manga.url}", headers) + + override fun chapterListParse(response: Response) = + response.parse<Series<Title>>().episodes!!.map { + SChapter.create().apply { + name = it.toString() + url = it.id.toString() + date_upload = it.timestamp + chapter_number = it.ord.toFloat() + } + } + + override fun fetchChapterList(manga: SManga) = + chapterListRequest(manga).fetch(::chapterListParse) + + override fun pageListRequest(chapter: SChapter) = + GET("$baseUrl/front/v1/episodes/${chapter.url}", headers) + + override fun pageListParse(response: Response) = + response.parse<Episode>().run { + if (!isLocked) return@run cutImages!! + error("This episode will be available $waitingTime.") + }.mapIndexed { idx, img -> Page(idx, "", img.toString()) } + + override fun fetchPageList(chapter: SChapter) = + pageListRequest(chapter).fetch(::pageListParse) + + override fun getFilterList() = FilterList(Category()) + + override fun latestUpdatesParse(response: Response) = + throw UnsupportedOperationException("Not used") + + override fun popularMangaRequest(page: Int) = + throw UnsupportedOperationException("Not used") + + override fun popularMangaParse(response: Response) = + throw UnsupportedOperationException("Not used") + + override fun imageUrlParse(response: Response) = + throw UnsupportedOperationException("Not used") + + private fun <R> Request.fetch(parse: (Response) -> R) = + client.newCall(this).asObservable().map { res -> + if (res.isSuccessful) return@map parse(res) + error(res.parse<Status>("status").toString()) + }!! + + private inline fun <reified T> Response.parse(key: String = "data") = + json.decodeFromJsonElement<T>( + json.parseToJsonElement(body!!.string()).jsonObject[key]!! + ) +} diff --git a/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaFilters.kt b/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaFilters.kt new file mode 100644 index 000000000..504803764 --- /dev/null +++ b/src/en/manta/src/eu/kanade/tachiyomi/extension/en/manta/MantaFilters.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.extension.en.manta + +import eu.kanade.tachiyomi.source.model.Filter + +private val categories = arrayOf( + "", + "New", + "Exclusive", + "Completed", + "Romance", + "BL / GL", + "Drama", + "Fantasy", + "Thriller", + "Slice of life" +) + +class Category( + values: Array<String> = categories +) : Filter.Select<String>("Category", values) + +inline val List<Filter<*>>.category: String + get() = (firstOrNull() as? Category)?.run { values[state] } ?: ""