diff --git a/src/en/nana/AndroidManifest.xml b/src/en/nana/AndroidManifest.xml new file mode 100644 index 000000000..55dea899b --- /dev/null +++ b/src/en/nana/AndroidManifest.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/src/en/nana/build.gradle b/src/en/nana/build.gradle new file mode 100644 index 000000000..ac4b4f8cd --- /dev/null +++ b/src/en/nana/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + extName = 'Nana' + pkgNameSuffix = 'en.nana' + extClass = '.Nana' + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/nana/res/mipmap-hdpi/ic_launcher.png b/src/en/nana/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..96ea4c54b Binary files /dev/null and b/src/en/nana/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/en/nana/res/mipmap-mdpi/ic_launcher.png b/src/en/nana/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..0e591e24e Binary files /dev/null and b/src/en/nana/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/en/nana/res/mipmap-xhdpi/ic_launcher.png b/src/en/nana/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..d3463fa1e Binary files /dev/null and b/src/en/nana/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/en/nana/res/mipmap-xxhdpi/ic_launcher.png b/src/en/nana/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d6bfe9780 Binary files /dev/null and b/src/en/nana/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/en/nana/res/mipmap-xxxhdpi/ic_launcher.png b/src/en/nana/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..724385309 Binary files /dev/null and b/src/en/nana/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/en/nana/res/web_hi_res_512.png b/src/en/nana/res/web_hi_res_512.png new file mode 100644 index 000000000..f999ef7fe Binary files /dev/null and b/src/en/nana/res/web_hi_res_512.png differ diff --git a/src/en/nana/src/eu/kanade/tachiyomi/extension/en/nana/Nana.kt b/src/en/nana/src/eu/kanade/tachiyomi/extension/en/nana/Nana.kt new file mode 100644 index 000000000..8d42cd659 --- /dev/null +++ b/src/en/nana/src/eu/kanade/tachiyomi/extension/en/nana/Nana.kt @@ -0,0 +1,185 @@ +package eu.kanade.tachiyomi.extension.en.nana + +import eu.kanade.tachiyomi.network.GET +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.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 rx.Observable + +class Nana : ParsedHttpSource() { + override val name = "Nana ナナ" + + override val baseUrl = "https://nana.my.id" + + override val lang = "en" + + override val supportsLatest = false + + override val client = super.client.newBuilder() + .rateLimit(1) + .build() + + // ~~Popular~~ Latest + override fun popularMangaRequest(page: Int): Request = + searchMangaRequest(page, "", FilterList()) + + 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 filterList = if (filters.isEmpty()) getFilterList() else filters + val tagsFilter = filterList.find { it is TagsFilter } as TagsFilter + val sortFilter = filterList.find { it is SortFilter } as SortFilter + + val url = "$baseUrl/".toHttpUrl().newBuilder() + .addQueryParameter("q", "${tagsFilter.toUriPart()} $query".trim()) + .addQueryParameter("sort", sortFilter.toUriPart()) + + if (page != 1) { + url.addQueryParameter("p", page.toString()) + } + + return GET(url.toString(), headers) + } + + override fun searchMangaSelector(): String = + "#thumbs_container > .id1" + + override fun searchMangaFromElement(element: Element): SManga = SManga.create().apply { + val a = element.selectFirst(".id3 > a") + setUrlWithoutDomain(a.absUrl("href")) + title = a.attr("title") + + val img = a.selectFirst("> img") + thumbnail_url = img.absUrl("src") + author = img.attr("alt") + .replace("$title by ", "") + .ifBlank { null } + + genre = element.select(".id4 > .tags > span") + .joinToString { it.text() } + + status = SManga.COMPLETED + initialized = true + } + + override fun searchMangaNextPageSelector(): String? = + "a.paginate_button.current + a.paginate_button" + + // Latest + override fun latestUpdatesRequest(page: Int): Request = + throw UnsupportedOperationException("Not used.") + + override fun latestUpdatesSelector(): String = + throw UnsupportedOperationException("Not used.") + + override fun latestUpdatesFromElement(element: Element): SManga = + throw UnsupportedOperationException("Not used.") + + override fun latestUpdatesNextPageSelector(): String? = + throw UnsupportedOperationException("Not used.") + + // Details + override fun fetchMangaDetails(manga: SManga): Observable = + Observable.just(manga) + + override fun mangaDetailsParse(document: Document): SManga = + throw UnsupportedOperationException("Not used.") + + // Chapters + override fun fetchChapterList(manga: SManga): Observable> { + return Observable.just( + listOf( + SChapter.create().apply { + setUrlWithoutDomain(manga.url) + name = "Chapter" + date_upload = 0L + chapter_number = 1F + } + ) + ) + } + + override fun chapterListSelector(): String = + throw UnsupportedOperationException("Not used.") + + override fun chapterFromElement(element: Element): SChapter = + throw UnsupportedOperationException("Not used.") + + // Pages + override fun pageListParse(document: Document): List { + val body = document.body().toString() + + return PATTERN_PAGES.find(body) + ?.groupValues?.get(1) + ?.split(',') + ?.map(String::trim) + ?.mapIndexed { i, imgStr -> + val imgUrl = baseUrl + imgStr.substring(1, imgStr.lastIndex) + Page(i, "", imgUrl) + } + ?: emptyList() + } + + override fun imageUrlParse(document: Document): String = + throw UnsupportedOperationException("Not used.") + + // Filters + override fun getFilterList(): FilterList = FilterList( + Filter.Header("Use comma (,) to separate tags"), + Filter.Header("Prefix plus (+) to require tag"), + Filter.Header("Prefix minus (-) to exclude tag"), + TagsFilter(), + + Filter.Separator(), + SortFilter(), + ) + + open class TagsFilter : + Filter.Text("Tags", "") { + fun toUriPart(): String { + return state.split(',') + .map(String::trim) + .map { tag -> + if (tag.isEmpty() || tag.contains('"')) { return@map tag } + + val prefix = tag.substring(0, 1) + + if (listOf("+", "-").any { prefix.contains(it) }) { + "$prefix\"${tag.substring(1)}\"" + } else { + "\"$tag\"" + } + } + .joinToString(" ") + } + } + + open class SortFilter : + Filter.Sort("Sort", arrayOf("Date Added"), Selection(0, false)) { + fun toUriPart(): String = when (state?.ascending) { + true -> "asc" + else -> "desc" + } + } + + // Other + companion object { + private val PATTERN_PAGES = Regex("Reader\\.pages\\s*=\\s*\\{\\\"pages\\\":\\[([^];\\n]+)]\\}\\.pages;") + } +}