diff --git a/purplecress/AndroidManifest.xml b/purplecress/AndroidManifest.xml
new file mode 100644
index 000000000..0d7e33d58
--- /dev/null
+++ b/purplecress/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/purplecress/build.gradle b/purplecress/build.gradle
new file mode 100644
index 000000000..9054d302b
--- /dev/null
+++ b/purplecress/build.gradle
@@ -0,0 +1,12 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'Purple Cress'
+ pkgNameSuffix = 'en.purplecress'
+ extClass = '.PurpleCress'
+ extVersionCode = 1
+ libVersion = '1.2'
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/purplecress/res/mipmap-hdpi/ic_launcher.png b/purplecress/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..7f6f6eb1c
Binary files /dev/null and b/purplecress/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/purplecress/res/mipmap-mdpi/ic_launcher.png b/purplecress/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..d1edc90ab
Binary files /dev/null and b/purplecress/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/purplecress/res/mipmap-xhdpi/ic_launcher.png b/purplecress/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..9a3537aa0
Binary files /dev/null and b/purplecress/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/purplecress/res/mipmap-xxhdpi/ic_launcher.png b/purplecress/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..1eabaa49b
Binary files /dev/null and b/purplecress/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/purplecress/res/mipmap-xxxhdpi/ic_launcher.png b/purplecress/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..8919f8318
Binary files /dev/null and b/purplecress/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/purplecress/res/web_hi_res_512.png b/purplecress/res/web_hi_res_512.png
new file mode 100644
index 000000000..0f46b816f
Binary files /dev/null and b/purplecress/res/web_hi_res_512.png differ
diff --git a/purplecress/src/eu/kanade/tachiyomi/extension/en/purplecress/PurpleCress.kt b/purplecress/src/eu/kanade/tachiyomi/extension/en/purplecress/PurpleCress.kt
new file mode 100644
index 000000000..70431ff1f
--- /dev/null
+++ b/purplecress/src/eu/kanade/tachiyomi/extension/en/purplecress/PurpleCress.kt
@@ -0,0 +1,133 @@
+package eu.kanade.tachiyomi.extension.en.purplecress
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.network.asObservableSuccess
+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.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import rx.Observable
+import java.text.SimpleDateFormat
+import java.util.Locale
+
+class PurpleCress : HttpSource() {
+ override val name = "Purple Cress"
+
+ override val baseUrl = "https://purplecress.com"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ override val client: OkHttpClient = network.cloudflareClient
+
+ override fun popularMangaRequest(page: Int) = GET(baseUrl)
+
+ override fun popularMangaParse(response: Response): MangasPage {
+ val seriesContainer = response.asJsoup().selectFirst("div.container-grid--small")
+ val mangaList: List = seriesContainer.select("a").map {
+ SManga.create().apply {
+ title = it.selectFirst("div.card__info").selectFirst("h3").html()
+ url = it.attr("href")
+ author = it.selectFirst("p.card__author").html().substringAfter("by ")
+ artist = author
+ description = it.attr("description")
+ thumbnail_url = it.selectFirst("img.image").attr("src")
+ status = when (it.selectFirst("h3.card__status").html()) {
+ "Ongoing" -> SManga.ONGOING
+ "Dropped" -> SManga.COMPLETED // Not sure what the best status is for "Dropped"
+ "Completed" -> SManga.COMPLETED // There aren't any completed series on the site, so I'm just guessing as to the string
+ else -> SManga.UNKNOWN
+ }
+ initialized = true // We have all the fields
+ }
+ }
+ return MangasPage(mangaList, false)
+ }
+
+ override fun latestUpdatesRequest(page: Int): Request = GET(baseUrl)
+
+ override fun latestUpdatesParse(response: Response): MangasPage {
+ val seriesContainer = response.asJsoup().selectFirst("div.container-grid--large")
+ val mangaList: List = seriesContainer.select("a").map {
+ SManga.create().apply {
+ title = it.selectFirst("h3.chapter__series-name").html()
+ url = it.attr("href").replaceFirst("chapter", "series").substringBeforeLast("/")
+ thumbnail_url = it.selectFirst("img.image").attr("src")
+ initialized = false
+ }
+ }
+ return MangasPage(mangaList, false)
+ }
+
+ override fun fetchMangaDetails(manga: SManga): Observable {
+ val oldUrl = manga.url
+ return client.newCall(mangaDetailsRequest(manga))
+ .asObservableSuccess()
+ .map { response ->
+ mangaDetailsParse(response).apply {
+ initialized = true
+ url = oldUrl // Sets URL in result to original URL
+ }
+ }
+ }
+
+ override fun mangaDetailsRequest(manga: SManga): Request = chapterListRequest(manga)
+
+ override fun mangaDetailsParse(response: Response): SManga {
+ val responseJ = response.asJsoup()
+ val infoBox = responseJ.selectFirst("div.series__info")
+ return SManga.create().apply {
+ title = infoBox.selectFirst("h1.series__name").html()
+ // url is set by overridden fetchMangaDetails
+ author = infoBox.selectFirst("p.series__author").html().substringAfter("by ")
+ artist = author
+ description = infoBox.selectFirst("p.description-pagagraph").html()
+ thumbnail_url = responseJ.selectFirst("img.thumbnail").attr("src")
+ status = when (infoBox.selectFirst("span.series__status").html()) {
+ "Ongoing" -> SManga.ONGOING
+ "Dropped" -> SManga.COMPLETED // See comments in popularMangaParse
+ "Completed" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ }
+ }
+
+ override fun chapterListParse(response: Response): List {
+ return response.asJsoup().select("a.chapter__card")
+ .map {
+ SChapter.create().apply {
+ url = it.attr("href")
+ name = it.selectFirst("span.chapter__name").html()
+ date_upload = it.selectFirst("h5.chapter__date").html()
+ .let { SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(it)?.time ?: 0L }
+ }
+ }
+ }
+
+ override fun pageListParse(response: Response): List {
+ return response.asJsoup().select("img.page__img").mapIndexed { index, element ->
+ Page(index, "", element.attr("src"))
+ }
+ }
+
+ override fun fetchImageUrl(page: Page): Observable {
+ return Observable.just(page.imageUrl)
+ }
+
+ override fun imageUrlRequest(page: Page): Request = throw UnsupportedOperationException("Not used")
+
+ override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
+
+ override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = Observable.just(MangasPage(emptyList(), false))
+
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException("Not used")
+
+ override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used")
+}