diff --git a/src/en/rizzcomic/AndroidManifest.xml b/src/en/rizzcomic/AndroidManifest.xml
new file mode 100644
index 000000000..568741e54
--- /dev/null
+++ b/src/en/rizzcomic/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/en/rizzcomic/build.gradle b/src/en/rizzcomic/build.gradle
new file mode 100644
index 000000000..dc8a6e4e3
--- /dev/null
+++ b/src/en/rizzcomic/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlinx-serialization'
+
+ext {
+ extName = 'Rizz Comic'
+ pkgNameSuffix = 'en.rizzcomic'
+ extClass = '.RizzComic'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
\ No newline at end of file
diff --git a/src/en/rizzcomic/res/mipmap-hdpi/ic_launcher.png b/src/en/rizzcomic/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..3240c622c
Binary files /dev/null and b/src/en/rizzcomic/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/rizzcomic/res/mipmap-mdpi/ic_launcher.png b/src/en/rizzcomic/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..8be812241
Binary files /dev/null and b/src/en/rizzcomic/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/rizzcomic/res/mipmap-xhdpi/ic_launcher.png b/src/en/rizzcomic/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..eb6609980
Binary files /dev/null and b/src/en/rizzcomic/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/rizzcomic/res/mipmap-xxhdpi/ic_launcher.png b/src/en/rizzcomic/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..6a2e222eb
Binary files /dev/null and b/src/en/rizzcomic/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/rizzcomic/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/rizzcomic/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..c99d7c610
Binary files /dev/null and b/src/en/rizzcomic/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/rizzcomic/res/web_hi_res_512.png b/src/en/rizzcomic/res/web_hi_res_512.png
new file mode 100644
index 000000000..a9e73db42
Binary files /dev/null and b/src/en/rizzcomic/res/web_hi_res_512.png differ
diff --git a/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt
new file mode 100644
index 000000000..f9ac3a6a9
--- /dev/null
+++ b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComic.kt
@@ -0,0 +1,281 @@
+package eu.kanade.tachiyomi.extension.en.rizzcomic
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.POST
+import eu.kanade.tachiyomi.network.asObservableSuccess
+import eu.kanade.tachiyomi.network.interceptor.rateLimit
+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 kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import okhttp3.FormBody
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class RizzComic : HttpSource() {
+
+ override val name = "Rizz Comic"
+
+ override val lang = "en"
+
+ override val baseUrl = "https://rizzcomic.com"
+
+ override val supportsLatest = true
+
+ private val json: Json by injectLazy()
+
+ override val client = network.cloudflareClient.newBuilder()
+ .rateLimit(1)
+ .build()
+
+ override fun headersBuilder() = super.headersBuilder()
+ .set("Referer", "$baseUrl/")
+
+ private val apiHeaders by lazy {
+ headersBuilder()
+ .set("X-Requested-With", "XMLHttpRequest")
+ .build()
+ }
+
+ private var urlPrefix: String? = null
+ private var genreCache: List> = emptyList()
+ private var attempts = 0
+
+ private fun updateCache() {
+ if ((urlPrefix.isNullOrEmpty() || genreCache.isEmpty()) && attempts < 3) {
+ runCatching {
+ val document = client.newCall(GET("$baseUrl/series", headers))
+ .execute().use { it.asJsoup() }
+
+ urlPrefix = document.selectFirst(".listupd a")
+ ?.attr("href")
+ ?.substringAfter("/series/")
+ ?.substringBefore("-")
+
+ genreCache = document.selectFirst(".filter .genrez")
+ ?.select("li")
+ .orEmpty()
+ .map {
+ val name = it.select("label").text()
+ val id = it.select("input").attr("value")
+
+ Pair(name, id)
+ }
+ }
+
+ attempts++
+ }
+ }
+
+ private fun getUrlPrefix(): String {
+ if (urlPrefix.isNullOrEmpty()) {
+ updateCache()
+ }
+
+ return urlPrefix!!
+ }
+
+ override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", SortFilter.POPULAR)
+ override fun popularMangaParse(response: Response) = searchMangaParse(response)
+
+ override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", SortFilter.LATEST)
+ override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ if (query.isNotEmpty()) {
+ val form = FormBody.Builder()
+ .add("search_value", query.trim())
+ .build()
+
+ return POST("$baseUrl/Index/live_search", apiHeaders, form)
+ }
+
+ val form = FormBody.Builder().apply {
+ filters.filterIsInstance().forEach {
+ it.addFormParameter(this)
+ }
+ }.build()
+
+ return POST("$baseUrl/Index/filter_series", apiHeaders, form)
+ }
+
+ override fun getFilterList(): FilterList {
+ val filters: MutableList> = mutableListOf(
+ Filter.Header("Filters don't work with text search"),
+ SortFilter(),
+ StatusFilter(),
+ TypeFilter(),
+ )
+
+ filters += if (genreCache.isEmpty()) {
+ listOf(
+ Filter.Separator(),
+ Filter.Header("Press reset to attempt to load genres"),
+ )
+ } else {
+ listOf(
+ GenreFilter(genreCache),
+ )
+ }
+
+ return FilterList(filters)
+ }
+
+ override fun searchMangaParse(response: Response): MangasPage {
+ updateCache()
+
+ val result = response.parseAs>()
+
+ val entries = result.map { comic ->
+ SManga.create().apply {
+ url = "${comic.slug}#${comic.id}"
+ title = comic.title
+ description = comic.synopsis
+ author = listOfNotNull(comic.author, comic.serialization).joinToString()
+ artist = comic.artist
+ status = comic.status.parseStatus()
+ thumbnail_url = comic.cover?.let { "$baseUrl/assets/images/$it" }
+ genre = buildList {
+ add(comic.type?.capitalize())
+ comic.genreIds?.onEach { gId ->
+ add(genreCache.firstOrNull { it.second == gId }?.first)
+ }
+ }.filterNotNull().joinToString()
+ initialized = true
+ }
+ }
+
+ return MangasPage(entries, false)
+ }
+
+ override fun fetchMangaDetails(manga: SManga): Observable {
+ return client.newCall(mangaDetailsRequest(manga))
+ .asObservableSuccess()
+ .map { mangaDetailsParse(it, manga) }
+ }
+
+ override fun mangaDetailsRequest(manga: SManga): Request {
+ val slug = manga.url.substringBefore("#")
+ val randomPart = getUrlPrefix()
+
+ return GET("$baseUrl/series/$randomPart-$slug", headers)
+ }
+
+ override fun getMangaUrl(manga: SManga): String {
+ val slug = manga.url.substringBefore("#")
+
+ val urlPart = urlPrefix?.let { "$it-" } ?: ""
+
+ return "$baseUrl/series/$urlPart$slug"
+ }
+
+ private fun mangaDetailsParse(response: Response, manga: SManga) = manga.apply {
+ val document = response.use { it.asJsoup() }
+
+ title = document.selectFirst("h1.entry-title")?.text().orEmpty()
+ artist = document.selectFirst(".tsinfo .imptdt:contains(artist) i")?.ownText()
+ author = listOfNotNull(
+ document.selectFirst(".tsinfo .imptdt:contains(author) i")?.ownText(),
+ document.selectFirst(".tsinfo .imptdt:contains(serialization) i")?.ownText(),
+ ).joinToString()
+ genre = buildList {
+ add(
+ document.selectFirst(".tsinfo .imptdt:contains(type) a")
+ ?.ownText()
+ ?.capitalize(),
+ )
+ document.select(".mgen a").eachText().onEach { add(it) }
+ }.filterNotNull().joinToString()
+ status = document.selectFirst(".tsinfo .imptdt:contains(status) i")?.text().parseStatus()
+ thumbnail_url = document.selectFirst(".infomanga > div[itemprop=image] img, .thumb img")?.absUrl("src")
+ }
+
+ private fun String?.parseStatus(): Int = when {
+ this == null -> SManga.UNKNOWN
+ listOf("ongoing", "publishing").any { contains(it, ignoreCase = true) } -> SManga.ONGOING
+ contains("hiatus", ignoreCase = true) -> SManga.ON_HIATUS
+ contains("completed", ignoreCase = true) -> SManga.COMPLETED
+ listOf("dropped", "cancelled").any { contains(it, ignoreCase = true) } -> SManga.CANCELLED
+ else -> SManga.UNKNOWN
+ }
+
+ override fun chapterListRequest(manga: SManga): Request {
+ val id = manga.url.substringAfter("#")
+ val slug = manga.url.substringBefore("#")
+
+ return GET("$baseUrl/index/search_chapters/$id#$slug", apiHeaders)
+ }
+
+ override fun chapterListParse(response: Response): List {
+ val result = response.parseAs>()
+ val slug = response.request.url.fragment!!
+
+ return result.map {
+ SChapter.create().apply {
+ url = "$slug-chapter-${it.name}"
+ name = "Chapter ${it.name}"
+ date_upload = runCatching {
+ dateFormat.parse(it.time!!)!!.time
+ }.getOrDefault(0L)
+ }
+ }
+ }
+
+ override fun pageListRequest(chapter: SChapter): Request {
+ return GET("$baseUrl/chapter/${getUrlPrefix()}-${chapter.url}", headers)
+ }
+
+ override fun pageListParse(response: Response): List {
+ val document = response.use { it.asJsoup() }
+ val chapterUrl = response.request.url.toString()
+
+ return document.select("div#readerarea img")
+ .mapIndexed { i, img ->
+ Page(i, chapterUrl, img.absUrl("src"))
+ }
+ }
+
+ override fun imageRequest(page: Page): Request {
+ val newHeaders = headersBuilder()
+ .set("Accept", "image/avif,image/webp,image/png,image/jpeg,*/*")
+ .set("Referer", page.url)
+ .build()
+
+ return GET(page.imageUrl!!, newHeaders)
+ }
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ throw UnsupportedOperationException("Not Used")
+ }
+
+ override fun imageUrlParse(response: Response): String {
+ throw UnsupportedOperationException("Not Used")
+ }
+
+ private inline fun Response.parseAs(): T =
+ use { it.body.string() }.let(json::decodeFromString)
+
+ companion object {
+ private fun String.capitalize() = replaceFirstChar {
+ if (it.isLowerCase()) {
+ it.titlecase(Locale.ROOT)
+ } else {
+ it.toString()
+ }
+ }
+
+ private val dateFormat by lazy {
+ SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
+ }
+ }
+}
diff --git a/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComicDto.kt b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComicDto.kt
new file mode 100644
index 000000000..f20d7454f
--- /dev/null
+++ b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComicDto.kt
@@ -0,0 +1,35 @@
+package eu.kanade.tachiyomi.extension.en.rizzcomic
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Comic(
+ val id: Int,
+ val title: String,
+ @SerialName("image_url") val cover: String? = null,
+ @SerialName("long_description") val synopsis: String? = null,
+ val status: String? = null,
+ val type: String? = null,
+ val artist: String? = null,
+ val author: String? = null,
+ val serialization: String? = null,
+ @SerialName("genre_id") val genres: String? = null,
+) {
+ val slug get() = title.trim().lowercase()
+ .replace(slugRegex, "-")
+ .replace("-s-", "s-")
+ .replace("-ll-", "ll-")
+
+ val genreIds get() = genres?.split(",")?.map(String::trim)
+
+ companion object {
+ private val slugRegex = Regex("""[^a-z0-9]+""")
+ }
+}
+
+@Serializable
+data class Chapter(
+ @SerialName("chapter_time") val time: String? = null,
+ @SerialName("chapter_title") val name: String,
+)
diff --git a/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComicFilters.kt b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComicFilters.kt
new file mode 100644
index 000000000..fd9426d51
--- /dev/null
+++ b/src/en/rizzcomic/src/eu/kanade/tachiyomi/extension/en/rizzcomic/RizzComicFilters.kt
@@ -0,0 +1,84 @@
+package eu.kanade.tachiyomi.extension.en.rizzcomic
+
+import eu.kanade.tachiyomi.source.model.Filter
+import eu.kanade.tachiyomi.source.model.FilterList
+import okhttp3.FormBody
+
+interface FormBodyFilter {
+ fun addFormParameter(form: FormBody.Builder)
+}
+
+abstract class SelectFilter(
+ name: String,
+ private val options: List>,
+ defaultValue: String? = null,
+) : FormBodyFilter, Filter.Select(
+ name,
+ options.map { it.first }.toTypedArray(),
+ options.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
+) {
+ abstract val formParameter: String
+ override fun addFormParameter(form: FormBody.Builder) {
+ form.add(formParameter, options[state].second)
+ }
+}
+
+class SortFilter(defaultOrder: String? = null) : SelectFilter("Sort By", sort, defaultOrder) {
+ override val formParameter = "OrderValue"
+ companion object {
+ private val sort = listOf(
+ Pair("Default", "all"),
+ Pair("A-Z", "title"),
+ Pair("Z-A", "titlereverse"),
+ Pair("Latest Update", "update"),
+ Pair("Latest Added", "latest"),
+ Pair("Popular", "popular"),
+ )
+
+ val POPULAR = FilterList(StatusFilter(), TypeFilter(), SortFilter("popular"))
+ val LATEST = FilterList(StatusFilter(), TypeFilter(), SortFilter("update"))
+ }
+}
+
+class StatusFilter : SelectFilter("Status", status) {
+ override val formParameter = "StatusValue"
+ companion object {
+ private val status = listOf(
+ Pair("All", "all"),
+ Pair("Ongoing", "ongoing"),
+ Pair("Complete", "completed"),
+ Pair("Hiatus", "hiatus"),
+ )
+ }
+}
+
+class TypeFilter : SelectFilter("Type", type) {
+ override val formParameter = "TypeValue"
+ companion object {
+ private val type = listOf(
+ Pair("All", "all"),
+ Pair("Manga", "Manga"),
+ Pair("Manhwa", "Manhwa"),
+ Pair("Manhua", "Manhua"),
+ Pair("Comic", "Comic"),
+ )
+ }
+}
+
+class CheckBoxFilter(
+ name: String,
+ val value: String,
+) : Filter.CheckBox(name)
+
+class GenreFilter(
+ genres: List>,
+) : FormBodyFilter, Filter.Group(
+ "Genre",
+ genres.map { CheckBoxFilter(it.first, it.second) },
+) {
+ override fun addFormParameter(form: FormBody.Builder) {
+ state.filter { it.state }.forEach {
+ form.add("genres_checked[]", it.value)
+ }
+ }
+}