diff --git a/src/it/demoneceleste/AndroidManifest.xml b/src/it/demoneceleste/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/it/demoneceleste/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/it/demoneceleste/build.gradle b/src/it/demoneceleste/build.gradle
new file mode 100644
index 000000000..30115fa18
--- /dev/null
+++ b/src/it/demoneceleste/build.gradle
@@ -0,0 +1,11 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'DemoneCeleste'
+ pkgNameSuffix = 'it.demoneceleste'
+ extClass = '.DemoneCeleste'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/it/demoneceleste/res/mipmap-hdpi/ic_launcher.png b/src/it/demoneceleste/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..818dfe130
Binary files /dev/null and b/src/it/demoneceleste/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/it/demoneceleste/res/mipmap-mdpi/ic_launcher.png b/src/it/demoneceleste/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..113eeb690
Binary files /dev/null and b/src/it/demoneceleste/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/it/demoneceleste/res/mipmap-xhdpi/ic_launcher.png b/src/it/demoneceleste/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..eef24c69b
Binary files /dev/null and b/src/it/demoneceleste/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/it/demoneceleste/res/mipmap-xxhdpi/ic_launcher.png b/src/it/demoneceleste/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6b9f85e51
Binary files /dev/null and b/src/it/demoneceleste/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/it/demoneceleste/res/mipmap-xxxhdpi/ic_launcher.png b/src/it/demoneceleste/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..983af7d70
Binary files /dev/null and b/src/it/demoneceleste/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/it/demoneceleste/res/web_hi_res_512.png b/src/it/demoneceleste/res/web_hi_res_512.png
new file mode 100644
index 000000000..d28b9d302
Binary files /dev/null and b/src/it/demoneceleste/res/web_hi_res_512.png differ
diff --git a/src/it/demoneceleste/src/eu/kanade/tachiyomi/extension/it/demoneceleste/DemoneCeleste.kt b/src/it/demoneceleste/src/eu/kanade/tachiyomi/extension/it/demoneceleste/DemoneCeleste.kt
new file mode 100644
index 000000000..5eec6fdd9
--- /dev/null
+++ b/src/it/demoneceleste/src/eu/kanade/tachiyomi/extension/it/demoneceleste/DemoneCeleste.kt
@@ -0,0 +1,283 @@
+package eu.kanade.tachiyomi.extension.it.demoneceleste
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.source.model.Filter
+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.ParsedHttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.FormBody
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class DemoneCeleste : ParsedHttpSource() {
+
+ override val name = "DemoneCeleste"
+ override val baseUrl = "https://www.demoneceleste.it/"
+ override val lang = "it"
+ override val supportsLatest = true
+ override val client: OkHttpClient = network.cloudflareClient
+
+ private val bgImgUrlRegex = """\((.*)\)""".toRegex()
+
+ //region REQUESTS
+
+ override fun popularMangaRequest(page: Int): Request = GET("$baseUrl/manga", headers)
+ override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/", headers)
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/search?sez=serie".toHttpUrlOrNull()!!.newBuilder()
+
+ if (query.isNotEmpty()) {
+ url.addQueryParameter("key", query)
+ } else {
+ url.addQueryParameter("key", "")
+ }
+
+ var status = ""
+
+ (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
+ when (filter) {
+ is GenreList -> {
+ val genreInclude = mutableListOf()
+ filter.state.forEach {
+ if (it.state) {
+ genreInclude.add(it.id)
+ }
+ }
+ if (genreInclude.isNotEmpty()) {
+ genreInclude.forEach { genre ->
+ url.addQueryParameter("tag[]", genre)
+ }
+ }
+ }
+ is StatusList -> {
+ var i = 0
+ filter.state.forEach {
+ if (it.state) {
+ status += "${if (i != 0) "-" else ""}${it.id}"
+ i++
+ }
+ }
+ }
+ }
+ }
+ return GET("$url#$status", headers)
+ }
+ //endregion
+
+ //region CONTENTS INFO
+ private fun mangasParse(response: Response, selector: String, num: Int): MangasPage {
+ val document = response.asJsoup()
+ var sele = selector
+
+ val encFrags = response.request.url.encodedFragment.toString().split('-')
+
+ if ((encFrags[0].isNotEmpty()) and (encFrags[0] != "null")) sele += ":matches(${encFrags.joinToString("|")})"
+
+ val mangas = document.select(sele).map { element ->
+ when (num) {
+ 1 -> popularMangaFromElement(element)
+ 2 -> latestUpdatesFromElement(element)
+ else -> searchMangaFromElement(element)
+ }
+ }
+ return MangasPage(mangas, false)
+ }
+ override fun popularMangaParse(response: Response): MangasPage = mangasParse(response, popularMangaSelector(), 1)
+ override fun latestUpdatesParse(response: Response): MangasPage = mangasParse(response, latestUpdatesSelector(), 2)
+ override fun searchMangaParse(response: Response): MangasPage = mangasParse(response, searchMangaSelector(), 3)
+
+ override fun popularMangaSelector() = ".col-md-6.row.no-pad:has(a.manga[href^=manga])"
+ override fun latestUpdatesSelector() = ".col.bg-light.ombra:has(a[href^=manga])"
+ override fun searchMangaSelector() = "div#serie .col-md-10.row.no-pad:has(h4 a[href^=manga])"
+
+ override fun popularMangaFromElement(element: Element): SManga = searchMangaFromElement(element)
+ override fun latestUpdatesFromElement(element: Element): SManga {
+ val manga = SManga.create()
+
+ manga.thumbnail_url = "$baseUrl/${(bgImgUrlRegex.find(element.select("a > div > div").first().attr("style")))!!.groupValues[1]}".replace("pub", "det")
+ element.select("a").first().let {
+ manga.setUrlWithoutDomain(it.attr("href"))
+ manga.title = it.text()
+ }
+
+ return manga
+ }
+ override fun searchMangaFromElement(element: Element): SManga {
+ val manga = SManga.create()
+
+ manga.thumbnail_url = "$baseUrl/${(bgImgUrlRegex.find(element.select(".col-md-auto.no-pad > a > div").first().attr("style")))!!.groupValues[1]}".replace("pub", "det")
+ element.select("a.manga[href^=manga]").first().let {
+ manga.setUrlWithoutDomain(it.attr("href"))
+ manga.title = it.text()
+ }
+ return manga
+ }
+
+ override fun mangaDetailsParse(document: Document): SManga {
+ val infoElement = document.select(".text-md-left.col-md-12.col-6.p-0")
+ val manga = SManga.create()
+
+ manga.status = when {
+ infoElement.text().lowercase().contains("in corso") -> SManga.ONGOING
+ infoElement.text().lowercase().contains("concluso") -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ manga.author = infoElement.select("p:has(strong:contains(Autore))")?.text()!!.replace("Autore: ", "")
+ manga.artist = infoElement.select("p:has(strong:contains(Artista))")?.text()!!.replace("Artista: ", "")
+ manga.genre = infoElement.select("p:has(strong:contains(Tag))")?.text()!!.replace("Tag: ", "")
+ manga.description = document.select(".text-justify")?.text().let {
+ if (it.isNullOrEmpty()) "Questo manga non รจ ancora stato pubblicato dagli scanner. Controlla tra un po'. Per cercare aggiornamenti riavvia la pagina." else it
+ }
+
+ return manga
+ }
+ //endregion
+
+ //region NEXT SELECTOR - Not used
+
+ override fun popularMangaNextPageSelector(): String? = null
+ override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
+ override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
+ //endregion
+
+ //region CHAPTER in CONTENTS INFO
+
+ override fun chapterListSelector() = "a[href^=\"manga/\"]:has(strong)"
+ override fun chapterFromElement(element: Element): SChapter {
+ val chapter = SChapter.create()
+
+ if (element.text().contains("Oneshot")) {
+ chapter.setUrlWithoutDomain(element.attr("href") + "#0")
+ chapter.name = element.text()
+ } else {
+ val container = element.parent().parent()
+
+ chapter.setUrlWithoutDomain("${element.attr("href")}#${element.parent().select("small").first().text().filter { it.isDigit() }.toInt()}")
+ chapter.name = container.parent().previousElementSibling().text().replace("""Capp.*""".toRegex(), " Ch.").replace("Volume", "Vol.") + element.text().replace(" #", " - ")
+ chapter.date_upload = SimpleDateFormat("dd-MM-yyyy", Locale.ITALY).parse(container.select("small").last().text().replace('/', '-'))!!.time
+ }
+
+ return chapter
+ }
+ override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
+ val basic = Regex("""Ch\.([0-9]+)""")
+ when {
+ basic.containsMatchIn(chapter.name) -> {
+ basic.find(chapter.name)?.let {
+ chapter.chapter_number = it.groups[1]?.value!!.toFloat()
+ }
+ }
+ }
+ }
+ //endregion
+
+ //region PAGE loading
+ override fun pageListRequest(chapter: SChapter): Request {
+ val (id, n) = chapter.url.replace("manga/", "").replace("""/#([0-9]+)""".toRegex(), "").split("/")
+ return POST(
+ "$baseUrl/ajax.php", headers,
+ FormBody.Builder().apply {
+ add("ajax", "pagine")
+ add("id", id)
+ add("n", n)
+ add("leggo", "1")
+ }.build()
+ )
+ }
+ override fun pageListParse(response: Response): List {
+ val body = response.body
+
+ val risultati = body!!.string().replace("", "").replace(""".*""".toRegex(), "").split("")
+ // The line above may be changed with this : - Not used because I couldn't find a way to use Regex's Global flag in Kotlin
+ // val results = """(.*?)""".toRegex().find(body!!.string())!!.groups
+ val pages = mutableListOf()
+
+ if (risultati.toString().contains("Accedi al sito")) {
+ pages.add(Page(1, "", "https://i.imgur.com/fiqTAUt.png"))
+ return pages
+ }
+
+ risultati.forEach {
+ pages.add(Page(risultati.indexOf(it), "", baseUrl + it))
+ }
+
+ return pages
+ }
+ override fun pageListParse(document: Document): List {
+ throw UnsupportedOperationException("Not used.")
+ }
+ //endregion
+
+ override fun imageUrlParse(document: Document) = ""
+ override fun imageRequest(page: Page): Request {
+ val imgHeader = Headers.Builder().apply {
+ add("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Build/KLP) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30")
+ add("Referer", baseUrl)
+ }.build()
+ return GET(page.imageUrl!!, imgHeader)
+ }
+
+ //region FILTERS
+ private class Genre(name: String, val id: String = name) : Filter.CheckBox(name)
+ private class GenreList(genres: List) : Filter.Group("Generi", genres)
+ private class Status(name: String, val id: String = name) : Filter.CheckBox(name, true)
+ private class StatusList(statuses: List) : Filter.Group("Stato", statuses)
+
+ override fun getFilterList() = FilterList(
+ StatusList(getStatusList()),
+ GenreList(getGenreList())
+ )
+
+ private fun getStatusList() = listOf(
+ Status("In corso", "in corso"),
+ Status("Finito", "concluso-Oneshot"),
+ Status("Sospeso", "sospeso")
+ )
+ private fun getGenreList() = listOf(
+ Genre("Avventura", "Avventura"),
+ Genre("Azione", "Azione"),
+ Genre("Color", "Color"),
+ Genre("Commedia", "Commedia"),
+ Genre("Crossdressing", "Crossdressing"),
+ Genre("Drammatico", "Drammatico"),
+ Genre("Ecchi", "Ecchi"),
+ Genre("Fantasy", "Fantasy"),
+ Genre("Harem", "Harem"),
+ Genre("Horror", "Horror"),
+ Genre("Isekai", "Isekai"),
+ Genre("Josei", "Josei"),
+ Genre("LongStrip", "LongStrip"),
+ Genre("Magia", "Magia"),
+ Genre("Manhua", "Manhua"),
+ Genre("Maturo", "Maturo"),
+ Genre("Mistero", "Mistero"),
+ Genre("Musica", "Musica"),
+ Genre("OneShot", "OneShot"),
+ Genre("Poliziesco", "Poliziesco"),
+ Genre("Psicologico", "Psicologico"),
+ Genre("Romantico", "Romantico"),
+ Genre("Sci-fi", "Sci-fi"),
+ Genre("Scolastico", "Scolastico"),
+ Genre("Seinen", "Seinen"),
+ Genre("Shonen", "Shonen"),
+ Genre("Shoujo", "Shoujo"),
+ Genre("Slice_of_Life", "Slice_of_Life"),
+ Genre("Soprannaturale", "Soprannaturale"),
+ Genre("Spokon", "Spokon"),
+ Genre("Sport", "Sport"),
+ Genre("Storico", "Storico")
+ )
+ //endregion
+}