diff --git a/src/fr/flamescansfr/AndroidManifest.xml b/src/fr/flamescansfr/AndroidManifest.xml
new file mode 100644
index 000000000..dbc41f53f
--- /dev/null
+++ b/src/fr/flamescansfr/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/fr/flamescansfr/build.gradle b/src/fr/flamescansfr/build.gradle
index 0a90ad7a2..58002e30f 100644
--- a/src/fr/flamescansfr/build.gradle
+++ b/src/fr/flamescansfr/build.gradle
@@ -1,9 +1,7 @@
ext {
extName = 'Legacy Scans'
extClass = '.LegacyScans'
- themePkg = 'mangathemesia'
- baseUrl = 'https://legacy-scans.com'
- overrideVersionCode = 0
+ extVersionCode = 31
}
apply from: "$rootDir/common.gradle"
diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt
index 466cdb0c7..9427e1c7a 100644
--- a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt
+++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScans.kt
@@ -1,9 +1,205 @@
package eu.kanade.tachiyomi.extension.fr.flamescansfr
-import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesia
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.network.interceptor.rateLimit
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
-class LegacyScans : MangaThemesia("Legacy Scans", "https://legacy-scans.com", "fr", dateFormat = SimpleDateFormat("MMMM d, yyyy", Locale.FRENCH)) {
- override val id = 8947802555328550956
+class LegacyScans : HttpSource() {
+ override val name: String = "Legacy Scans"
+
+ override val lang: String = "fr"
+
+ override val baseUrl: String = "https://legacy-scans.com"
+
+ override val supportsLatest: Boolean = true
+
+ override val versionId: Int = 2
+
+ override val client = network.cloudflareClient.newBuilder()
+ .rateLimit(3)
+ .build()
+
+ private val json: Json by injectLazy()
+
+ override fun popularMangaRequest(page: Int) = GET("$apiUrl/misc/views/all", headers)
+
+ override fun popularMangaParse(response: Response) =
+ mangasPageParse(response.parseAs>(), false)
+
+ override fun latestUpdatesRequest(page: Int): Request {
+ val offset = pageOffset(page)
+ val url = "$apiUrl/misc/comic/home/updates".toHttpUrl().newBuilder()
+ .addQueryParameter("start", "${offset.first}")
+ .addQueryParameter("end", "${offset.second}")
+ .build()
+ return GET(url, headers)
+ }
+
+ override fun latestUpdatesParse(response: Response): MangasPage =
+ mangasPageParse(response.parseAs>())
+
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
+ return if (query.startsWith(URL_SEARCH_PREFIX)) {
+ val manga = SManga.create().apply {
+ url = "/comics/${query.substringAfter(URL_SEARCH_PREFIX)}"
+ }
+ client.newCall(mangaDetailsRequest(manga)).asObservableSuccess().map { response ->
+ val document = response.asJsoup()
+ when {
+ isMangaPage(document) -> {
+ MangasPage(listOf(mangaDetailsParse(document)), false)
+ }
+ else -> MangasPage(emptyList(), false)
+ }
+ }
+ } else {
+ super.fetchSearchManga(page, query, filters)
+ }
+ }
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ if (query.isNotBlank()) {
+ val url = "$apiUrl/misc/home/search".toHttpUrl().newBuilder()
+ .addQueryParameter("title", query)
+ .build()
+ return GET(url, headers)
+ }
+
+ val defaultSearchOffSet = 18
+
+ val offset = pageOffset(page, defaultSearchOffSet)
+
+ val url = "$apiUrl/misc/comic/search/query".toHttpUrl().newBuilder()
+ .addQueryParameter("start", "${offset.first}")
+ .addQueryParameter("end", "${offset.second}")
+
+ filters.forEach { filter ->
+ when (filter) {
+ is SelectFilter -> {
+ val selected = filter.selectedValue()
+ if (selected.isBlank()) return@forEach
+ url.addQueryParameter(filter.field, selected)
+ }
+ is GenreList -> {
+ val genres = filter.state
+ .filter(GenreCheckBox::state)
+ .joinToString(",") { it.name }
+ if (genres.isBlank()) return@forEach
+ url.addQueryParameter("genreNames", genres)
+ }
+ else -> {}
+ }
+ }
+
+ return GET(url.build(), headers)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val pathSegments = response.request.url.pathSegments
+ return when {
+ pathSegments.contains("comic") -> {
+ mangasPageParse(response.parseAs().comics)
+ }
+ else -> {
+ mangasPageParse(response.parseAs().results, false)
+ }
+ }
+ }
+
+ val mangaDetailsDescriptionSelector = ".serieDescription p"
+
+ override fun mangaDetailsParse(response: Response): SManga =
+ mangaDetailsParse(response.asJsoup())
+
+ override fun chapterListParse(response: Response): List {
+ val document = response.asJsoup()
+ return document.select(".chapterList a").map { element ->
+ SChapter.create().apply {
+ val spans = element.select("span").map { it.text() }
+ name = spans.first()
+ date_upload = spans.last().toDate()
+ setUrlWithoutDomain(element.absUrl("href"))
+ }
+ }
+ }
+
+ override fun pageListParse(response: Response): List {
+ val document = response.asJsoup()
+ return document.select(".readerMainContainer img").mapIndexed { index, image ->
+ Page(index, document.location(), image.absUrl("src"))
+ }
+ }
+
+ override fun imageUrlParse(response: Response): String = ""
+
+ override fun getFilterList(): FilterList {
+ return FilterList(
+ SelectFilter("Status", "status", statusList),
+ SelectFilter("Type", "type", typesList),
+ GenreList("Genres", genresList),
+ )
+ }
+
+ private fun mangaDetailsParse(document: Document): SManga {
+ return with(document.selectFirst(".serieContainer")!!) {
+ SManga.create().apply {
+ title = selectFirst("h1")!!.text()
+ thumbnail_url = selectFirst("img")?.absUrl("src")
+ genre = select(".serieGenre span").joinToString { it.text() }
+ description = selectFirst(mangaDetailsDescriptionSelector)?.text()
+ author = selectFirst(".serieAdd p:contains(produit) strong")?.text()
+ artist = selectFirst(".serieAdd p:contains(Auteur) strong")?.text()
+ setUrlWithoutDomain(document.location())
+ }
+ }
+ }
+
+ private fun pageOffset(page: Int, max: Int = 28): Pair {
+ val start = max * (page - 1) + 1
+ val end = max * page
+ return start to end
+ }
+
+ private fun mangasPageParse(dto: List, hasNextPage: Boolean = true): MangasPage {
+ val mangas = dto.map {
+ SManga.create().apply {
+ title = it.title
+ thumbnail_url = it.cover?.let { cover -> "$apiUrl/$cover" }
+ url = "/comics/${it.slug}"
+ }
+ }
+ return MangasPage(mangas, hasNextPage && mangas.isNotEmpty())
+ }
+
+ private inline fun Response.parseAs(): T =
+ json.decodeFromString(body.string())
+
+ private fun isMangaPage(document: Document): Boolean =
+ document.selectFirst(mangaDetailsDescriptionSelector) != null
+
+ private fun String.toDate(): Long =
+ try { dateFormat.parse(trim())!!.time } catch (_: Exception) { 0L }
+
+ companion object {
+ const val apiUrl = "https://api.legacy-scans.com"
+ const val URL_SEARCH_PREFIX = "slug:"
+ val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.FRENCH)
+ }
}
diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansDto.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansDto.kt
new file mode 100644
index 000000000..f15592417
--- /dev/null
+++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansDto.kt
@@ -0,0 +1,20 @@
+package eu.kanade.tachiyomi.extension.fr.flamescansfr
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+class MangaDto(
+ val cover: String?,
+ val slug: String,
+ val title: String,
+)
+
+@Serializable
+class SearchDto(
+ val comics: List,
+)
+
+@Serializable
+class SearchQueryDto(
+ val results: List,
+)
diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansFilter.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansFilter.kt
new file mode 100644
index 000000000..bab29f739
--- /dev/null
+++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansFilter.kt
@@ -0,0 +1,111 @@
+package eu.kanade.tachiyomi.extension.fr.flamescansfr
+
+import eu.kanade.tachiyomi.source.model.Filter
+
+class SelectFilter(name: String, val field: String, values: Array) :
+ Filter.Select(name, values) {
+ fun selectedValue() = values[state]
+}
+
+class GenreCheckBox(name: String) : Filter.CheckBox(name)
+
+class GenreList(title: String, genres: List) : Filter.Group(
+ title,
+ genres.map(::GenreCheckBox),
+)
+
+val typesList = arrayOf("", "Manga", "Manhua", "Manhwa", "One shot")
+
+val statusList = arrayOf("", "En cours", "Terminé", "Annulé", "En pause", "Abandonné")
+
+val genresList = listOf(
+ "Action",
+ "Aventure",
+ "Arts Martiaux",
+ "Combat",
+ "Comédie",
+ "Drame",
+ "Fantastique",
+ "Science-fiction",
+ "Shonen",
+ "Amitié",
+ "Amour",
+ "Romance",
+ "Shôjo",
+ "Tranches de vie",
+ "Harem",
+ "Surnaturel",
+ "Guerre",
+ "Mystère",
+ "Fantaisie",
+ "Psychologique",
+ "Mature",
+ "Tragédie",
+ "Webtoons",
+ "Seinen",
+ "Historique",
+ "Vie scolaire",
+ "magie",
+ "One Shot",
+ "Shôjo Ai",
+ "Ecchi",
+ "Horreur",
+ "Gender Bender",
+ "Adulte",
+ "Josei",
+ "Returner",
+ "comedy",
+ "Sports",
+ "Monstres",
+ "Realité Virtuel",
+ "Mecha",
+ "isekaï",
+ "Jeux vidéo",
+ "Inconnu",
+ "Doujinshi",
+ "Policier",
+ "Réincarnation",
+ "Yuri",
+ "Sport",
+ "crime",
+ "Gangster",
+ "Police",
+ "Organisation secrète",
+ "Magical Girls",
+ "Bromance",
+ "Adventure",
+ "Necromancer",
+ "Shônen Ai",
+ "Boxe",
+ "Parodie",
+ "Hentai",
+ "4-koma",
+ "Voyage Temporel",
+ "vampires",
+ "Super héros",
+ "A",
+ "c",
+ "t",
+ "i",
+ "o",
+ "n",
+ ",",
+ " ",
+ "r",
+ "s",
+ "M",
+ "a",
+ "u",
+ "x",
+ "v",
+ "e",
+ "C",
+ "m",
+ "b",
+ "S",
+ "h",
+ "Smut",
+ "démons",
+ "Virtuel world",
+ "Vengeance",
+)
diff --git a/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansUrlActivity.kt b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansUrlActivity.kt
new file mode 100644
index 000000000..ab28d83fb
--- /dev/null
+++ b/src/fr/flamescansfr/src/eu/kanade/tachiyomi/extension/fr/flamescansfr/LegacyScansUrlActivity.kt
@@ -0,0 +1,37 @@
+package eu.kanade.tachiyomi.extension.fr.flamescansfr
+
+import android.app.Activity
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import kotlin.system.exitProcess
+
+class LegacyScansUrlActivity : Activity() {
+
+ private val tag = javaClass.simpleName
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val pathSegments = intent?.data?.pathSegments
+ if (pathSegments != null && pathSegments.size > 1) {
+ val item = pathSegments[1]
+ val mainIntent = Intent().apply {
+ action = "eu.kanade.tachiyomi.SEARCH"
+ putExtra("query", "${LegacyScans.URL_SEARCH_PREFIX}$item")
+ putExtra("filter", packageName)
+ }
+
+ try {
+ startActivity(mainIntent)
+ } catch (e: ActivityNotFoundException) {
+ Log.e(tag, e.toString())
+ }
+ } else {
+ Log.e(tag, "could not parse uri from intent $intent")
+ }
+
+ finish()
+ exitProcess(0)
+ }
+}