diff --git a/src/en/vyvymanga/AndroidManifest.xml b/src/en/vyvymanga/AndroidManifest.xml
new file mode 100644
index 000000000..b4571bfa8
--- /dev/null
+++ b/src/en/vyvymanga/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/src/en/vyvymanga/build.gradle b/src/en/vyvymanga/build.gradle
new file mode 100644
index 000000000..4a39d90af
--- /dev/null
+++ b/src/en/vyvymanga/build.gradle
@@ -0,0 +1,11 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+ext {
+ extName = 'VyvyManga'
+ pkgNameSuffix = 'en.vyvymanga'
+ extClass = '.VyvyManga'
+ extVersionCode = 1
+}
+
+apply from: "$rootDir/common.gradle"
diff --git a/src/en/vyvymanga/res/mipmap-hdpi/ic_launcher.png b/src/en/vyvymanga/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..5db1f69a0
Binary files /dev/null and b/src/en/vyvymanga/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/src/en/vyvymanga/res/mipmap-mdpi/ic_launcher.png b/src/en/vyvymanga/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..5bc11d78a
Binary files /dev/null and b/src/en/vyvymanga/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/src/en/vyvymanga/res/mipmap-xhdpi/ic_launcher.png b/src/en/vyvymanga/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..247ffb80d
Binary files /dev/null and b/src/en/vyvymanga/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src/en/vyvymanga/res/mipmap-xxhdpi/ic_launcher.png b/src/en/vyvymanga/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..24498e972
Binary files /dev/null and b/src/en/vyvymanga/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src/en/vyvymanga/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/vyvymanga/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..e4b3d44e6
Binary files /dev/null and b/src/en/vyvymanga/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src/en/vyvymanga/res/web_hi_res_512.png b/src/en/vyvymanga/res/web_hi_res_512.png
new file mode 100644
index 000000000..88e9d9e41
Binary files /dev/null and b/src/en/vyvymanga/res/web_hi_res_512.png differ
diff --git a/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyManga.kt b/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyManga.kt
new file mode 100644
index 000000000..536cefb57
--- /dev/null
+++ b/src/en/vyvymanga/src/eu/kanade/tachiyomi/extension/en/vyvymanga/VyvyManga.kt
@@ -0,0 +1,148 @@
+package eu.kanade.tachiyomi.extension.en.vyvymanga
+
+import eu.kanade.tachiyomi.network.GET
+import eu.kanade.tachiyomi.source.model.FilterList
+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 okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.Request
+import org.jsoup.nodes.Document
+import org.jsoup.nodes.Element
+import java.text.ParseException
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+
+class VyvyManga : ParsedHttpSource() {
+ override val name = "VyvyManga"
+
+ override val baseUrl = "https://vyvymanga.com"
+
+ override val lang = "en"
+
+ override val supportsLatest = true
+
+ private val dateFormat = SimpleDateFormat("MMM dd, yyy", Locale.US)
+
+ // Popular
+ override fun popularMangaRequest(page: Int): Request =
+ GET("$baseUrl/search", headers)
+
+ override fun popularMangaSelector(): String =
+ searchMangaSelector()
+
+ override fun popularMangaFromElement(element: Element): SManga =
+ searchMangaFromElement(element)
+
+ override fun popularMangaNextPageSelector(): String? =
+ searchMangaNextPageSelector()
+
+ // Search
+ override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
+ val url = "$baseUrl/search".toHttpUrl().newBuilder()
+ .addQueryParameter("q", query)
+ .addQueryParameter("page", page.toString())
+
+ return GET(url.toString(), headers)
+ }
+
+ override fun searchMangaSelector(): String =
+ ".comic-item"
+
+ override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply {
+ setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
+ title = element.selectFirst(".comic-title")!!.text()
+ thumbnail_url = element.selectFirst(".comic-image")!!.absUrl("data-background-image")
+ }
+
+ override fun searchMangaNextPageSelector(): String? =
+ "[rel=next]"
+
+ // Latest
+ override fun latestUpdatesRequest(page: Int): Request =
+ GET("$baseUrl/search?q=&completed=&sort=updated_at", headers)
+
+ override fun latestUpdatesSelector(): String =
+ searchMangaSelector()
+
+ override fun latestUpdatesFromElement(element: Element): SManga =
+ searchMangaFromElement(element)
+
+ override fun latestUpdatesNextPageSelector(): String? =
+ searchMangaNextPageSelector()
+
+ // Details
+ override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
+ title = document.selectFirst("h1")!!.text()
+ artist = document.selectFirst(".pre-title:contains(Artist) ~ a")?.text()
+ author = document.selectFirst(".pre-title:contains(Author) ~ a")?.text()
+ description = document.selectFirst(".summary > .content")!!.text()
+ genre = document.select(".pre-title:contains(Genres) ~ a").joinToString { it.text() }
+ status = when (document.selectFirst(".pre-title:contains(Status) ~ span:not(.space)")?.text()) {
+ "Ongoing" -> SManga.ONGOING
+ "Completed" -> SManga.COMPLETED
+ else -> SManga.UNKNOWN
+ }
+ thumbnail_url = document.selectFirst(".img-manga").absUrl("src")
+ }
+
+ // Chapters
+ override fun chapterListSelector(): String =
+ ".list-group > a"
+
+ override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
+ url = element.absUrl("href")
+ name = element.ownText()
+ date_upload = parseChapterDate(element.selectFirst("> p")?.text())
+ }
+
+ // Pages
+ override fun pageListRequest(chapter: SChapter): Request =
+ GET(chapter.url, headers)
+
+ override fun pageListParse(document: Document): List {
+ return document.select("img.d-block").mapIndexed { index, element ->
+ Page(index, "", element.absUrl("data-src"))
+ }
+ }
+
+ override fun imageUrlParse(document: Document): String {
+ throw UnsupportedOperationException("Not used.")
+ }
+
+ // Other
+ // Date logic lifted from Madara
+ private fun parseChapterDate(date: String?): Long {
+ date ?: return 0
+
+ fun SimpleDateFormat.tryParse(string: String): Long {
+ return try {
+ parse(string)?.time ?: 0
+ } catch (_: ParseException) {
+ 0
+ }
+ }
+
+ return when {
+ "ago".endsWith(date) -> {
+ parseRelativeDate(date)
+ }
+ else -> dateFormat.tryParse(date)
+ }
+ }
+
+ private fun parseRelativeDate(date: String): Long {
+ val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
+ val cal = Calendar.getInstance()
+
+ return when {
+ date.contains("day") -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
+ date.contains("hour") -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
+ date.contains("minute") -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
+ date.contains("second") -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
+ else -> 0
+ }
+ }
+}