diff --git a/src/en/lynxscans/AndroidManifest.xml b/src/en/lynxscans/AndroidManifest.xml new file mode 100644 index 000000000..30deb7f79 --- /dev/null +++ b/src/en/lynxscans/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/src/en/lynxscans/build.gradle b/src/en/lynxscans/build.gradle new file mode 100644 index 000000000..4b62f029f --- /dev/null +++ b/src/en/lynxscans/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlinx-serialization' + +ext { + extName = 'LynxScans' + pkgNameSuffix = 'en.lynxscans' + extClass = '.LynxScans' + extVersionCode = 7 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/lynxscans/res/mipmap-hdpi/ic_launcher.png b/src/en/lynxscans/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..1dd6cfde4 Binary files /dev/null and b/src/en/lynxscans/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/lynxscans/res/mipmap-mdpi/ic_launcher.png b/src/en/lynxscans/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..1c411bc85 Binary files /dev/null and b/src/en/lynxscans/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/lynxscans/res/mipmap-xhdpi/ic_launcher.png b/src/en/lynxscans/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..f9083e6c8 Binary files /dev/null and b/src/en/lynxscans/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/lynxscans/res/mipmap-xxhdpi/ic_launcher.png b/src/en/lynxscans/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..951a16c8f Binary files /dev/null and b/src/en/lynxscans/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/lynxscans/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/lynxscans/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5e87a4ede Binary files /dev/null and b/src/en/lynxscans/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/lynxscans/res/web_hi_res_512.png b/src/en/lynxscans/res/web_hi_res_512.png new file mode 100644 index 000000000..65950d349 Binary files /dev/null and b/src/en/lynxscans/res/web_hi_res_512.png differ diff --git a/src/en/lynxscans/src/eu/kanade/tachiyomi/extension/en/lynxscans/LynxScans.kt b/src/en/lynxscans/src/eu/kanade/tachiyomi/extension/en/lynxscans/LynxScans.kt new file mode 100644 index 000000000..a151a0a20 --- /dev/null +++ b/src/en/lynxscans/src/eu/kanade/tachiyomi/extension/en/lynxscans/LynxScans.kt @@ -0,0 +1,130 @@ +package eu.kanade.tachiyomi.extension.en.lynxscans + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.Request +import okhttp3.Response +import uy.kohesive.injekt.injectLazy + +class LynxScans : HttpSource() { + override val baseUrl: String = "https://lynxscans.com" + private val apiUrl: String = "https://api.lynxscans.com/api" + + override val lang: String = "en" + override val name: String = "LynxScans" + + override val versionId = 2 + + override val supportsLatest: Boolean = true + + private val json: Json by injectLazy() + + // Popular + override fun popularMangaRequest(page: Int): Request { + return GET("$apiUrl/comics?page=$page", headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val data = json.decodeFromString(response.body.string()) + + val titles = data.comics.data.map(PopularComicsData::toSManga) + + return MangasPage(titles, !data.comics.next_page_url.isNullOrEmpty()) + } + + // Latest + override fun latestUpdatesRequest(page: Int): Request { + return GET("$apiUrl/latest?page=$page", headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val data = json.decodeFromString(response.body.string()) + + val titles = data.chapters.data.distinctBy { it.comic_titleSlug }.map(LatestChaptersData::toSManga) + + return MangasPage(titles, !data.chapters.next_page_url.isNullOrEmpty()) + } + + // Search + override fun searchMangaParse(response: Response): MangasPage { + throw UnsupportedOperationException("Not used") + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + throw Exception("Search is not supported") + } + + // Details + override fun mangaDetailsParse(response: Response): SManga { + val data = json.decodeFromString(response.body.string()) + + return data.comic.toSManga() + } + + override fun mangaDetailsRequest(manga: SManga): Request { + val titleId = manga.url.substringAfterLast("/") + + return GET("$apiUrl/comics/$titleId", headers) + } + + override fun getMangaUrl(manga: SManga): String { + val titleId = manga.url.substringAfterLast("/") + + return "$baseUrl/comics/$titleId" + } + + // Chapters + override fun chapterListRequest(manga: SManga): Request { + return mangaDetailsRequest(manga) + } + + override fun chapterListParse(response: Response): List { + val data = json.decodeFromString(response.body.string()) + + val chapters: MutableList = mutableListOf() + + data.comic.volumes.forEach { volume -> + volume.chapters.forEach { chapter -> + chapters.add( + SChapter.create().apply { + url = "/comics/${data.comic.titleSlug}/volume/${volume.number}/chapter/${chapter.number}" + name = volume.name + " " + (if (!chapter.name.contains("chapter", true)) "Chapter ${chapter.number} " else "") + chapter.name + }, + ) + } + } + + return chapters + } + + override fun getChapterUrl(chapter: SChapter): String { + val chapterPath = chapter.url.substringAfter("/") + + return "$baseUrl/$chapterPath" + } + + // Page + override fun pageListRequest(chapter: SChapter): Request { + val chapterPath = chapter.url.substringAfter("/") + + return GET("$apiUrl/$chapterPath", headers) + } + + override fun pageListParse(response: Response): List { + val data = json.decodeFromString(response.body.string()) + + return data.pages.mapIndexed { idx, it -> + Page(idx, imageUrl = it.thumb) + } + } + + // Unused + override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not Used") +} diff --git a/src/en/lynxscans/src/eu/kanade/tachiyomi/extension/en/lynxscans/LynxScansDto.kt b/src/en/lynxscans/src/eu/kanade/tachiyomi/extension/en/lynxscans/LynxScansDto.kt new file mode 100644 index 000000000..81780f5c5 --- /dev/null +++ b/src/en/lynxscans/src/eu/kanade/tachiyomi/extension/en/lynxscans/LynxScansDto.kt @@ -0,0 +1,110 @@ +package eu.kanade.tachiyomi.extension.en.lynxscans + +import eu.kanade.tachiyomi.source.model.SManga +import kotlinx.serialization.Serializable + +@Serializable +data class Latest( + val chapters: LatestChapters, +) + +@Serializable +data class LatestChapters( + val next_page_url: String?, + val data: List, +) + +@Serializable +data class LatestChaptersData( + val comic_title: String, + val comic_thumb: String, + val comic_titleSlug: String, +) { + fun toSManga(): SManga = SManga.create().apply { + title = this@LatestChaptersData.comic_title + thumbnail_url = this@LatestChaptersData.comic_thumb + url = "/comics/" + this@LatestChaptersData.comic_titleSlug + } +} + +@Serializable +data class Popular( + val comics: PopularComics, +) + +@Serializable +data class PopularComics( + val next_page_url: String?, + val data: List, +) + +@Serializable +data class PopularComicsData( + val title: String, + val thumb: String, + val titleSlug: String, +) { + fun toSManga(): SManga = SManga.create().apply { + title = this@PopularComicsData.title + thumbnail_url = this@PopularComicsData.thumb + url = "/comics/" + this@PopularComicsData.titleSlug + } +} + +@Serializable +data class MangaDetails( + val comic: MangaDetailsComicData, +) + +@Serializable +data class MangaDetailsComicData( + val title: String, + val thumb: String, + val titleSlug: String, + val artist: String, + val author: String, + val description: String, + val tags: List, + + val volumes: List, + +) { + fun toSManga(): SManga = SManga.create().apply { + title = this@MangaDetailsComicData.title + thumbnail_url = this@MangaDetailsComicData.thumb + url = "/comics/" + this@MangaDetailsComicData.titleSlug + author = if (this@MangaDetailsComicData.author != "blank") this@MangaDetailsComicData.author else null + artist = if (this@MangaDetailsComicData.artist != "blank") this@MangaDetailsComicData.artist else null + description = this@MangaDetailsComicData.description + genre = this@MangaDetailsComicData.tags.joinToString { it.name } + status = SManga.UNKNOWN + } +} + +@Serializable +data class MangaDetailsTag( + val name: String, +) + +@Serializable +data class MangaDetailsVolume( + val chapters: List, + val name: String, + val number: Int, +) + +@Serializable +data class MangaDetailsChapter( + val name: String, + val number: Int, +) + +@Serializable +data class PageList( + val pages: List, +) + +@Serializable +data class Page( + val thumb: String, +)