diff --git a/src/all/comicfury/AndroidManifest.xml b/src/all/comicfury/AndroidManifest.xml
new file mode 100644
index 000000000..30deb7f79
--- /dev/null
+++ b/src/all/comicfury/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/src/all/comicfury/build.gradle b/src/all/comicfury/build.gradle
new file mode 100644
index 000000000..daf7c52bd
--- /dev/null
+++ b/src/all/comicfury/build.gradle
@@ -0,0 +1,17 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Comic Fury'
+ pkgNameSuffix = 'all.comicfury'
+ extClass = '.ComicFuryFactory'
+ extVersionCode = 1
+ isNsfw = true
+}
+
+dependencies {
+ implementation(project(':lib-textinterceptor'))
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/all/comicfury/res/mipmap-hdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..865ccdc5f
Binary files /dev/null and b/src/all/comicfury/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/all/comicfury/res/mipmap-mdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..436075be2
Binary files /dev/null and b/src/all/comicfury/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/all/comicfury/res/mipmap-xhdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..2e7393ab7
Binary files /dev/null and b/src/all/comicfury/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/all/comicfury/res/mipmap-xxhdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..a5c573216
Binary files /dev/null and b/src/all/comicfury/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/all/comicfury/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/comicfury/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..1188a64c5
Binary files /dev/null and b/src/all/comicfury/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/all/comicfury/res/web_hi_res_512.png b/src/all/comicfury/res/web_hi_res_512.png
new file mode 100644
index 000000000..9f721a498
Binary files /dev/null and b/src/all/comicfury/res/web_hi_res_512.png differ
diff --git a/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFury.kt b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFury.kt
new file mode 100644
index 000000000..dc7b3b82a
--- /dev/null
+++ b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFury.kt
@@ -0,0 +1,290 @@
+package eu.kanade.tachiyomi.extension.all.comicfury
+
+import android.app.Application
+import android.content.SharedPreferences
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
+import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.ConfigurableSource
+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.HttpSource
+import eu.kanade.tachiyomi.util.asJsoup
+import okhttp3.HttpUrl
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import okhttp3.Response
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class ComicFury(
+ override val lang: String,
+ private val siteLang: String = lang, // override lang string used in MangaSearch
+ private val extraName: String = "",
+) : HttpSource(), ConfigurableSource {
+ override val baseUrl: String = "https://comicfury.com"
+ override val name: String = "Comic Fury$extraName" //Used for No Text
+ override val supportsLatest: Boolean = true
+ private val dateFormat = SimpleDateFormat("dd MMM yyyy hh:mm aa", Locale.US)
+ private val dateFormatSlim = SimpleDateFormat("dd MMM yyyy", Locale.US)
+
+ override val client = super.client.newBuilder().addInterceptor(TextInterceptor()).build()
+
+ /**
+ * Archive is on a separate page from manga info
+ */
+ override fun chapterListRequest(manga: SManga): Request =
+ GET("$baseUrl/read/${manga.url.substringAfter("?url=")}/archive")
+
+ /**
+ * Open Archive Url instead of the details page
+ * Helps with getting past the nfsw pages
+ */
+ override fun getMangaUrl(manga: SManga): String {
+ return "$baseUrl/read/" + manga.url.substringAfter("?url=") + "/archive"
+ }
+
+ /**
+ * There are two different ways chapters are setup
+ * First Way if (true)
+ * Manga -> Chapter -> Comic -> Pages
+ * The Second Way if (false)
+ * Manga -> Comic -> Pages
+ *
+ * Importantly the Chapter And Comic Pages can be easy distinguished
+ * by the name of the list elements in this case archive-chapter/archive-comic
+ *
+ * For Manga that doesn't have "chapters" skip the loop. Including All Sub-Comics of Chapters
+ *
+ * Put the chapter name into scanlator so read can know what chapter it is.
+ *
+ * Chapter Number is handled as Chapter dot Comic. Ex. Chapter 6, Comic 4: chapter_number = 6.4
+ *
+ */
+ override fun chapterListParse(response: Response): List {
+ val jsp = response.asJsoup()
+ if (jsp.selectFirst("div.archive-chapter") != null) {
+ val chapters: MutableList = arrayListOf()
+ for (chapter in jsp.select("div.archive-chapter").parents().reversed()) {
+ val name = chapter.text()
+ chapters.addAll(
+ client.newCall(
+ GET("$baseUrl${chapter.attr("href")}"),
+ ).execute()
+ .use { chapterListParse(it) }
+ .mapIndexed { i, it ->
+ it.apply {
+ scanlator = name
+ chapter_number += i
+ }
+ },
+ )
+ }
+ return chapters
+ } else {
+ return jsp.select("div.archive-comic").mapIndexed { i, it ->
+ SChapter.create().apply {
+ url = it.parent()!!.attr("href")
+ name = it.child(0).ownText()
+ date_upload = it.child(1).ownText().toDate()
+ chapter_number = "0.$i".toFloat()
+ }
+ }.toList().reversed()
+ }
+ }
+
+ override fun pageListParse(response: Response): List {
+ val jsp = response.asJsoup()
+ val pages: MutableList = arrayListOf()
+ val comic = jsp.selectFirst("div.is--comic-page")
+ for (child in comic!!.select("div.is--image-segment div img")) {
+ pages.add(
+ Page(
+ pages.size,
+ response.request.url.toString(),
+ child.attr("src"),
+ ),
+ )
+ }
+ if (showAuthorsNotesPref()) {
+ for (child in comic.select("div.is--author-notes div.is--comment-box").withIndex()) {
+ pages.add(
+ Page(
+ pages.size,
+ response.request.url.toString(),
+ TextInterceptorHelper.createUrl(
+ jsp.selectFirst("a.is--comment-author")?.ownText()
+ ?: "Error No Author For Comment Found",
+ jsp.selectFirst("div.is--comment-content")?.html()
+ ?: "Error No Comment Content Found",
+ ),
+ ),
+ )
+ }
+ }
+ return pages
+ }
+
+ /**
+ * Author name joining maybe redundant.
+ *
+ * Manga Status is available but not currently implemented.
+ */
+ override fun mangaDetailsParse(response: Response): SManga {
+ val jsp = response.asJsoup()
+ val desDiv = jsp.selectFirst("div.description-tags")
+ return SManga.create().apply {
+ setUrlWithoutDomain(response.request.url.toString())
+ description = desDiv?.parent()?.ownText()
+ genre = desDiv?.children()?.eachText()?.joinToString(", ")
+ author = jsp.select("a.authorname").eachText().joinToString(", ")
+ initialized = true
+ }
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ val jsp = response.asJsoup()
+ val list: MutableList = arrayListOf()
+ for (result in jsp.select("div.webcomic-result")) {
+ list.add(
+ SManga.create().apply {
+ url = result.selectFirst("div.webcomic-result-avatar a")!!.attr("href")
+ title = result.selectFirst("div.webcomic-result-title")!!.attr("title")
+ thumbnail_url = result.selectFirst("div.webcomic-result-avatar a img")!!.absUrl("src")
+ },
+ )
+ }
+ return MangasPage(list, (jsp.selectFirst("div.search-next-page") != null))
+ }
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val req: HttpUrl.Builder = "$baseUrl/search.php".toHttpUrl().newBuilder()
+ req.addQueryParameter("page", page.toString())
+ req.addQueryParameter("language", siteLang)
+ filters.forEach {
+ when (it) {
+ is TagsFilter -> req.addEncodedQueryParameter(
+ "tags",
+ it.state.replace(", ", ","),
+ )
+ is SortFilter -> req.addQueryParameter("sort", it.state.toString())
+ is CompletedComicFilter -> req.addQueryParameter(
+ "completed",
+ it.state.toInt().toString(),
+ )
+ is LastUpdatedFilter -> req.addQueryParameter(
+ "lastupdate",
+ it.state.toString(),
+ )
+ is ViolenceFilter -> req.addQueryParameter("fv", it.state.toString())
+ is NudityFilter -> req.addQueryParameter("fn", it.state.toString())
+ is StrongLangFilter -> req.addQueryParameter("fl", it.state.toString())
+ is SexualFilter -> req.addQueryParameter("fs", it.state.toString())
+ else -> {}
+ }
+ }
+
+ return Request.Builder().url(req.build()).build()
+ }
+
+ // START OF AUTHOR NOTES //
+ private val preferences: SharedPreferences by lazy {
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ }
+ companion object {
+ private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
+ }
+ private fun showAuthorsNotesPref() =
+ preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
+
+ override fun setupPreferenceScreen(screen: PreferenceScreen) {
+ val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
+ key = SHOW_AUTHORS_NOTES_KEY; title = "Show author's notes"
+ summary = "Enable to see the author's notes at the end of chapters (if they're there)."
+ setDefaultValue(false)
+ }
+ screen.addPreference(authorsNotesPref)
+ }
+ // END OF AUTHOR NOTES //
+
+ // START OF FILTERS //
+ override fun getFilterList(): FilterList = getFilterList(0)
+ private fun getFilterList(sortIndex: Int): FilterList = FilterList(
+ TagsFilter(),
+ Filter.Separator(),
+ SortFilter(sortIndex),
+ Filter.Separator(),
+ LastUpdatedFilter(),
+ CompletedComicFilter(),
+ Filter.Separator(),
+ Filter.Header("Flags"),
+ ViolenceFilter(),
+ NudityFilter(),
+ StrongLangFilter(),
+ SexualFilter(),
+ )
+
+ internal class SortFilter(index: Int) : Filter.Select(
+ "Sort By",
+ arrayOf("Relevance", "Popularity", "Last Update"),
+ index,
+ )
+ internal class CompletedComicFilter : Filter.CheckBox("Comic Completed", false)
+ internal class LastUpdatedFilter : Filter.Select(
+ "Last Updated",
+ arrayOf("All Time", "This Week", "This Month", "This Year", "Completed Only"),
+ 0,
+ )
+ internal class ViolenceFilter : Filter.Select(
+ "Violence",
+ arrayOf("None / Minimal", "Violent Content", "Gore / Graphic"),
+ 2,
+ )
+ internal class NudityFilter : Filter.Select(
+ "Frontal Nudity",
+ arrayOf("None", "Occasional", "Frequent"),
+ 2,
+ )
+ internal class StrongLangFilter : Filter.Select(
+ "Strong Language",
+ arrayOf("None", "Occasional", "Frequent"),
+ 2,
+ )
+ internal class SexualFilter : Filter.Select(
+ "Sexual Content",
+ arrayOf("No Sexual Content", "Sexual Situations", "Strong Sexual Themes"),
+ 2,
+ )
+ internal class TagsFilter : Filter.Text("Tags")
+
+ // END OF FILTERS //
+
+ override fun popularMangaRequest(page: Int): Request =
+ searchMangaRequest(page, "", getFilterList(1))
+ override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
+
+ override fun latestUpdatesRequest(page: Int): Request =
+ searchMangaRequest(page, "", getFilterList(2))
+ override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
+
+ override fun imageUrlParse(response: Response): String =
+ throw UnsupportedOperationException("Not Used")
+
+ private fun String.toDate(): Long {
+ val ret = this.replace("st", "")
+ .replace("nd", "")
+ .replace("rd", "")
+ .replace("th", "")
+ .replace(",", "")
+ return dateFormat.parse(ret)?.time ?: dateFormatSlim.parse(ret)!!.time
+ }
+
+ private fun Boolean.toInt(): Int = if (this) { 0 } else { 1 }
+}
diff --git a/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFuryFactory.kt b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFuryFactory.kt
new file mode 100644
index 000000000..cb18b496d
--- /dev/null
+++ b/src/all/comicfury/src/eu/kanade/tachiyomi/extension/all/comicfury/ComicFuryFactory.kt
@@ -0,0 +1,23 @@
+package eu.kanade.tachiyomi.extension.all.comicfury
+
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceFactory
+
+class ComicFuryFactory : SourceFactory {
+ override fun createSources(): List