From 2fb3b505355c8635d0a64c6fcfb28f884169a0e5 Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Sun, 28 Aug 2016 22:59:00 +0200 Subject: [PATCH] Add genre filter for catalogue (#428) * Add genre filter for catalogue * Implement genre filter for batoto * hardcode filters for sources * swtich filter id to string * reset filters when switching sources * Add filter support to mangafox * Catalogue changes * Indefinite snackbar on error, use plain subscriptions in catalogue presenter --- .../eu/kanade/tachiyomi/data/source/Source.kt | 1 - .../data/source/online/OnlineSource.kt | 22 +- .../data/source/online/ParsedOnlineSource.kt | 3 +- .../data/source/online/YamlOnlineSource.kt | 11 +- .../data/source/online/english/Batoto.kt | 74 +++++- .../data/source/online/english/Kissmanga.kt | 77 +++++- .../data/source/online/english/Mangafox.kt | 43 +++- .../data/source/online/english/Mangahere.kt | 2 +- .../data/source/online/english/Mangasee.kt | 2 +- .../source/online/english/Readmangatoday.kt | 10 +- .../data/source/online/german/WieManga.kt | 2 +- .../data/source/online/russian/Mangachan.kt | 2 +- .../data/source/online/russian/Mintmanga.kt | 2 +- .../data/source/online/russian/Readmanga.kt | 2 +- .../ui/catalogue/CatalogueFragment.kt | 96 +++++--- .../tachiyomi/ui/catalogue/CataloguePager.kt | 41 ++++ .../ui/catalogue/CataloguePresenter.kt | 232 +++++++++--------- .../java/eu/kanade/tachiyomi/util/RxPager.kt | 21 -- .../main/res/layout/fragment_catalogue.xml | 85 ++++--- app/src/main/res/menu/catalogue_list.xml | 6 + app/src/main/res/values/strings.xml | 1 + 21 files changed, 484 insertions(+), 251 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt index d7fd5c5c1..ba196a51f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/Source.kt @@ -47,5 +47,4 @@ interface Source { * @param page the page. */ fun fetchImage(page: Page): Observable - } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt index fa9759b82..8daa87703 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/OnlineSource.kt @@ -58,6 +58,11 @@ abstract class OnlineSource(context: Context) : Source { */ val headers by lazy { headersBuilder().build() } + /** + * Genre filters. + */ + val filters by lazy { getFilterList() } + /** * Default network client for doing requests. */ @@ -126,11 +131,11 @@ abstract class OnlineSource(context: Context) : Source { * the current page and the next page url. * @param query the search query. */ - open fun fetchSearchManga(page: MangasPage, query: String): Observable = client - .newCall(searchMangaRequest(page, query)) + open fun fetchSearchManga(page: MangasPage, query: String, filters: List): Observable = client + .newCall(searchMangaRequest(page, query, filters)) .asObservable() .map { response -> - searchMangaParse(response, page, query) + searchMangaParse(response, page, query, filters) page } @@ -141,9 +146,9 @@ abstract class OnlineSource(context: Context) : Source { * @param page the page object. * @param query the search query. */ - open protected fun searchMangaRequest(page: MangasPage, query: String): Request { + open protected fun searchMangaRequest(page: MangasPage, query: String, filters: List): Request { if (page.page == 1) { - page.url = searchMangaInitialUrl(query) + page.url = searchMangaInitialUrl(query, filters) } return GET(page.url, headers) } @@ -153,7 +158,7 @@ abstract class OnlineSource(context: Context) : Source { * * @param query the search query. */ - abstract protected fun searchMangaInitialUrl(query: String): String + abstract protected fun searchMangaInitialUrl(query: String, filters: List): String /** * Parse the response from the site. It should add a list of manga and the absolute url to the @@ -163,7 +168,7 @@ abstract class OnlineSource(context: Context) : Source { * @param page the page object to be filled. * @param query the search query. */ - abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String) + abstract protected fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) /** * Returns an observable with the updated details for a manga. Normally it's not needed to @@ -428,4 +433,7 @@ abstract class OnlineSource(context: Context) : Source { } + data class Filter(val id: String, val name: String) + + open fun getFilterList(): List = emptyList() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt index 2c2b8de14..639438d44 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/ParsedOnlineSource.kt @@ -64,7 +64,7 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) { * @param page the page object to be filled. * @param query the search query. */ - override fun searchMangaParse(response: Response, page: MangasPage, query: String) { + override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) { val document = response.asJsoup() for (element in document.select(searchMangaSelector())) { Manga.create(id).apply { @@ -179,5 +179,4 @@ abstract class ParsedOnlineSource(context: Context) : OnlineSource(context) { * @param document the parsed document. */ abstract protected fun imageUrlParse(document: Document): String - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt index 21f8c5919..70ba85b5f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/YamlOnlineSource.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.GET import eu.kanade.tachiyomi.data.network.POST +import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.getLanguages import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.Page @@ -14,6 +15,7 @@ import okhttp3.Request import okhttp3.Response import org.jsoup.Jsoup import org.jsoup.nodes.Element +import rx.Observable import java.text.SimpleDateFormat import java.util.* @@ -68,9 +70,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con } } - override fun searchMangaRequest(page: MangasPage, query: String): Request { + override fun searchMangaRequest(page: MangasPage, query: String, filters: List): Request { if (page.page == 1) { - page.url = searchMangaInitialUrl(query) + page.url = searchMangaInitialUrl(query, filters) } return when (map.search.method?.toLowerCase()) { "post" -> POST(page.url, headers, map.search.createForm()) @@ -78,9 +80,9 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con } } - override fun searchMangaInitialUrl(query: String) = map.search.url.replace("\$query", query) + override fun searchMangaInitialUrl(query: String, filters: List) = map.search.url.replace("\$query", query) - override fun searchMangaParse(response: Response, page: MangasPage, query: String) { + override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) { val document = response.asJsoup() for (element in document.select(map.search.manga_css)) { Manga.create(id).apply { @@ -184,5 +186,4 @@ class YamlOnlineSource(context: Context, mappings: Map<*, *>) : OnlineSource(con throw Exception("image_regex and image_css are null") } } - } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt index 0fe674241..40a296880 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Batoto.kt @@ -84,9 +84,21 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex override fun popularMangaNextPageSelector() = "#show_more_row" - override fun searchMangaInitialUrl(query: String) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=1" + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/search_ajax?name=${Uri.encode(query)}&order_cond=views&order=desc&p=1&genre_cond=and&genres=${getFilterParams(filters)}" - override fun searchMangaParse(response: Response, page: MangasPage, query: String) { + private fun getFilterParams(filters: List): String = filters + .map { + ";i" + it.id + }.joinToString() + + override fun searchMangaRequest(page: MangasPage, query: String, filters: List): Request { + if (page.page == 1) { + page.url = searchMangaInitialUrl(query, filters) + } + return GET(page.url, headers) + } + + override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) { val document = response.asJsoup() for (element in document.select(searchMangaSelector())) { Manga.create(id).apply { @@ -96,7 +108,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex } page.nextPageUrl = document.select(searchMangaNextPageSelector()).first()?.let { - "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}" + "$baseUrl/search_ajax?name=${Uri.encode(query)}&p=${page.page + 1}&order_cond=views&order=desc&genre_cond=and&genres=" + getFilterParams(filters) } } @@ -211,7 +223,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex val start = pageUrl.indexOf("#") + 1 val end = pageUrl.indexOf("_", start) val id = pageUrl.substring(start, end) - return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end+1)}", pageHeaders) + return GET("$baseUrl/areader?id=$id&p=${pageUrl.substring(end + 1)}", pageHeaders) } override fun imageUrlParse(document: Document): String { @@ -219,10 +231,10 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex } override fun login(username: String, password: String) = - client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers)) - .asObservable() - .flatMap { doLogin(it, username, password) } - .map { isAuthenticationSuccessful(it) } + client.newCall(GET("$baseUrl/forums/index.php?app=core&module=global§ion=login", headers)) + .asObservable() + .flatMap { doLogin(it, username, password) } + .map { isAuthenticationSuccessful(it) } private fun doLogin(response: Response, username: String, password: String): Observable { val doc = response.asJsoup() @@ -242,7 +254,7 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex } override fun isAuthenticationSuccessful(response: Response) = - response.priorResponse() != null && response.priorResponse().code() == 302 + response.priorResponse() != null && response.priorResponse().code() == 302 override fun isLogged(): Boolean { return network.cookies.get(URI(baseUrl)).any { it.name() == "pass_hash" } @@ -264,4 +276,48 @@ class Batoto(context: Context, override val id: Int) : ParsedOnlineSource(contex } } + // [...document.querySelectorAll("#advanced_options div.genre_buttons")].map((el,i) => { + // const onClick=el.getAttribute('onclick');const id=onClick.substr(14,onClick.length-16);return `Filter("${id}", "${el.textContent.trim()}")` + // }).join(',\n') + // on https://bato.to/search + override fun getFilterList(): List = listOf( + Filter("40", "4-Koma"), + Filter("1", "Action"), + Filter("2", "Adventure"), + Filter("39", "Award Winning"), + Filter("3", "Comedy"), + Filter("41", "Cooking"), + Filter("9", "Doujinshi"), + Filter("10", "Drama"), + Filter("12", "Ecchi"), + Filter("13", "Fantasy"), + Filter("15", "Gender Bender"), + Filter("17", "Harem"), + Filter("20", "Historical"), + Filter("22", "Horror"), + Filter("34", "Josei"), + Filter("27", "Martial Arts"), + Filter("30", "Mecha"), + Filter("42", "Medical"), + Filter("37", "Music"), + Filter("4", "Mystery"), + Filter("38", "Oneshot"), + Filter("5", "Psychological"), + Filter("6", "Romance"), + Filter("7", "School Life"), + Filter("8", "Sci-fi"), + Filter("32", "Seinen"), + Filter("35", "Shoujo"), + Filter("16", "Shoujo Ai"), + Filter("33", "Shounen"), + Filter("19", "Shounen Ai"), + Filter("21", "Slice of Life"), + Filter("23", "Smut"), + Filter("25", "Sports"), + Filter("26", "Supernatural"), + Filter("28", "Tragedy"), + Filter("36", "Webtoon"), + Filter("29", "Yaoi"), + Filter("31", "Yuri") + ) } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt index 163712425..565cbefb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Kissmanga.kt @@ -42,22 +42,34 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con override fun popularMangaNextPageSelector() = "li > a:contains(› Next)" - override fun searchMangaRequest(page: MangasPage, query: String): Request { + override fun searchMangaRequest(page: MangasPage, query: String, filters: List): Request { if (page.page == 1) { - page.url = searchMangaInitialUrl(query) + page.url = searchMangaInitialUrl(query, filters) } val form = FormBody.Builder().apply { add("authorArtist", "") add("mangaName", query) add("status", "") - add("genres", "") - }.build() + } - return POST(page.url, headers, form) + val filterIndexes = filters.map { it.id.toInt() } + val maxFilterIndex = filterIndexes.max() + + if (maxFilterIndex !== null) { + for (i in 0..maxFilterIndex) { + form.add("genres", if (filterIndexes.contains(i)) { + "1" + } else { + "0" + }) + } + } + + return POST(page.url, headers, form.build()) } - override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch" + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/AdvanceSearch" override fun searchMangaSelector() = popularMangaSelector() @@ -73,7 +85,7 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con manga.author = infoElement.select("p:has(span:contains(Author:)) > a").first()?.text() manga.genre = infoElement.select("p:has(span:contains(Genres:)) > *:gt(0)").text() manga.description = infoElement.select("p:has(span:contains(Summary:)) ~ p").text() - manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it)} + manga.status = infoElement.select("p:has(span:contains(Status:))").first()?.text().orEmpty().let { parseStatus(it) } manga.thumbnail_url = document.select(".rightBox:eq(0) img").first()?.attr("src") } @@ -109,10 +121,59 @@ class Kissmanga(context: Context, override val id: Int) : ParsedOnlineSource(con } // Not used - override fun pageListParse(document: Document, pages: MutableList) {} + override fun pageListParse(document: Document, pages: MutableList) { + } override fun imageUrlRequest(page: Page) = GET(page.url) override fun imageUrlParse(document: Document) = "" + // $("select[name=\"genres\"]").map((i,el) => `Filter("${i}", "${$(el).next().text().trim()}")`).get().join(',\n') + // on http://kissmanga.com/AdvanceSearch + override fun getFilterList(): List = listOf( + Filter("0", "Action"), + Filter("1", "Adult"), + Filter("2", "Adventure"), + Filter("3", "Comedy"), + Filter("4", "Comic"), + Filter("5", "Cooking"), + Filter("6", "Doujinshi"), + Filter("7", "Drama"), + Filter("8", "Ecchi"), + Filter("9", "Fantasy"), + Filter("10", "Gender Bender"), + Filter("11", "Harem"), + Filter("12", "Historical"), + Filter("13", "Horror"), + Filter("14", "Josei"), + Filter("15", "Lolicon"), + Filter("16", "Manga"), + Filter("17", "Manhua"), + Filter("18", "Manhwa"), + Filter("19", "Martial Arts"), + Filter("20", "Mature"), + Filter("21", "Mecha"), + Filter("22", "Medical"), + Filter("23", "Music"), + Filter("24", "Mystery"), + Filter("25", "One shot"), + Filter("26", "Psychological"), + Filter("27", "Romance"), + Filter("28", "School Life"), + Filter("29", "Sci-fi"), + Filter("30", "Seinen"), + Filter("31", "Shotacon"), + Filter("32", "Shoujo"), + Filter("33", "Shoujo Ai"), + Filter("34", "Shounen"), + Filter("35", "Shounen Ai"), + Filter("36", "Slice of Life"), + Filter("37", "Smut"), + Filter("38", "Sports"), + Filter("39", "Supernatural"), + Filter("40", "Tragedy"), + Filter("41", "Webtoon"), + Filter("42", "Yaoi"), + Filter("43", "Yuri") + ) } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt index 17d9f1045..db79dc1e8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangafox.kt @@ -36,8 +36,8 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont override fun popularMangaNextPageSelector() = "a:has(span.next)" - override fun searchMangaInitialUrl(query: String) = - "$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1" + override fun searchMangaInitialUrl(query: String, filters: List) = + "$baseUrl/search.php?name_method=cw&advopts=1&order=za&sort=views&name=$query&page=1&${filters.map { it.id + "=1" }.joinToString("&")}" override fun searchMangaSelector() = "table#listing > tbody > tr:gt(0)" @@ -118,4 +118,43 @@ class Mangafox(context: Context, override val id: Int) : ParsedOnlineSource(cont override fun imageUrlParse(document: Document) = document.getElementById("image").attr("src") + // $('select.genres').map((i,el)=>`Filter("${$(el).attr('name')}", "${$(el).next().text().trim()}")`).get().join(',\n') + // on http://kissmanga.com/AdvanceSearch + override fun getFilterList(): List = listOf( + Filter("genres[Action]", "Action"), + Filter("genres[Adult]", "Adult"), + Filter("genres[Adventure]", "Adventure"), + Filter("genres[Comedy]", "Comedy"), + Filter("genres[Doujinshi]", "Doujinshi"), + Filter("genres[Drama]", "Drama"), + Filter("genres[Ecchi]", "Ecchi"), + Filter("genres[Fantasy]", "Fantasy"), + Filter("genres[Gender Bender]", "Gender Bender"), + Filter("genres[Harem]", "Harem"), + Filter("genres[Historical]", "Historical"), + Filter("genres[Horror]", "Horror"), + Filter("genres[Josei]", "Josei"), + Filter("genres[Martial Arts]", "Martial Arts"), + Filter("genres[Mature]", "Mature"), + Filter("genres[Mecha]", "Mecha"), + Filter("genres[Mystery]", "Mystery"), + Filter("genres[One Shot]", "One Shot"), + Filter("genres[Psychological]", "Psychological"), + Filter("genres[Romance]", "Romance"), + Filter("genres[School Life]", "School Life"), + Filter("genres[Sci-fi]", "Sci-fi"), + Filter("genres[Seinen]", "Seinen"), + Filter("genres[Shoujo]", "Shoujo"), + Filter("genres[Shoujo Ai]", "Shoujo Ai"), + Filter("genres[Shounen]", "Shounen"), + Filter("genres[Shounen Ai]", "Shounen Ai"), + Filter("genres[Slice of Life]", "Slice of Life"), + Filter("genres[Smut]", "Smut"), + Filter("genres[Sports]", "Sports"), + Filter("genres[Supernatural]", "Supernatural"), + Filter("genres[Tragedy]", "Tragedy"), + Filter("genres[Webtoons]", "Webtoons"), + Filter("genres[Yaoi]", "Yaoi"), + Filter("genres[Yuri]", "Yuri") + ) } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt index dd63a5d13..7da6487a7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangahere.kt @@ -34,7 +34,7 @@ class Mangahere(context: Context, override val id: Int) : ParsedOnlineSource(con override fun popularMangaNextPageSelector() = "div.next-page > a.next" - override fun searchMangaInitialUrl(query: String) = + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/search.php?name=$query&page=1&sort=views&order=za" override fun searchMangaSelector() = "div.result_search > dl:has(dt)" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt index 7cc85e813..3874f2aff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Mangasee.kt @@ -47,7 +47,7 @@ class Mangasee(context: Context, override val id: Int) : ParsedOnlineSource(cont override fun popularMangaNextPageSelector() = "ul.pagination > li > a:contains(Next)" - override fun searchMangaInitialUrl(query: String) = + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/advanced-search/result.php?sortBy=alphabet&direction=ASC&textOnly=no&resPerPage=20&page=1&seriesName=$query" override fun searchMangaSelector() = "div.row > div > div > div > h1" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt index 7c89ada96..7cd682792 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/english/Readmangatoday.kt @@ -6,8 +6,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.network.POST import eu.kanade.tachiyomi.data.source.EN import eu.kanade.tachiyomi.data.source.Language +import eu.kanade.tachiyomi.data.source.Source import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.Page +import eu.kanade.tachiyomi.data.source.online.OnlineSource import eu.kanade.tachiyomi.data.source.online.ParsedOnlineSource import okhttp3.OkHttpClient import okhttp3.Request @@ -38,16 +40,16 @@ class Readmangatoday(context: Context, override val id: Int) : ParsedOnlineSourc override fun popularMangaNextPageSelector() = "div.hot-manga > ul.pagination > li > a:contains(»)" - override fun searchMangaInitialUrl(query: String) = + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/search" - override fun searchMangaRequest(page: MangasPage, query: String): Request { + override fun searchMangaRequest(page: MangasPage, query: String, filters: List): Request { if (page.page == 1) { - page.url = searchMangaInitialUrl(query) + page.url = searchMangaInitialUrl(query, filters) } - var builder = okhttp3.FormBody.Builder() + val builder = okhttp3.FormBody.Builder() builder.add("query", query) return POST(page.url, headers, builder.build()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt index d58e5af55..f3f48ab17 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/german/WieManga.kt @@ -36,7 +36,7 @@ class WieManga(context: Context, override val id: Int) : ParsedOnlineSource(cont override fun popularMangaNextPageSelector() = null - override fun searchMangaInitialUrl(query: String) = "$baseUrl/search/?wd=$query" + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/search/?wd=$query" override fun searchMangaSelector() = ".searchresult td > div" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt index b93cc0867..48e5cb924 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mangachan.kt @@ -23,7 +23,7 @@ class Mangachan(context: Context, override val id: Int) : ParsedOnlineSource(con override fun popularMangaInitialUrl() = "$baseUrl/mostfavorites" - override fun searchMangaInitialUrl(query: String) = "$baseUrl/?do=search&subaction=search&story=$query" + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/?do=search&subaction=search&story=$query" override fun popularMangaSelector() = "div.content_row" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt index 25abf0082..ff73f608f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Mintmanga.kt @@ -24,7 +24,7 @@ class Mintmanga(context: Context, override val id: Int) : ParsedOnlineSource(con override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate" - override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query" + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/search?q=$query" override fun popularMangaSelector() = "div.desc" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt index 7bb6cc50f..b645db2d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/russian/Readmanga.kt @@ -24,7 +24,7 @@ class Readmanga(context: Context, override val id: Int) : ParsedOnlineSource(con override fun popularMangaInitialUrl() = "$baseUrl/list?sortType=rate" - override fun searchMangaInitialUrl(query: String) = "$baseUrl/search?q=$query" + override fun searchMangaInitialUrl(query: String, filters: List) = "$baseUrl/search?q=$query" override fun popularMangaSelector() = "div.desc" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt index ae62b22b5..e50bb26f4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt @@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.catalogue import android.content.res.Configuration import android.os.Bundle +import android.support.design.widget.Snackbar import android.support.v7.widget.GridLayoutManager import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.SearchView import android.support.v7.widget.Toolbar import android.view.* import android.view.animation.AnimationUtils -import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.ProgressBar import android.widget.Spinner @@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.DividerItemDecoration import eu.kanade.tachiyomi.widget.EndlessScrollListener +import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener import kotlinx.android.synthetic.main.fragment_catalogue.* import kotlinx.android.synthetic.main.toolbar.* import nucleus.factory.RequiresPresenter @@ -64,7 +65,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold /** * Query of the search box. */ - private val query: String? + private val query: String get() = presenter.query /** @@ -92,11 +93,6 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold */ private var numColumnsSubscription: Subscription? = null - /** - * Display mode of the catalogue (list or grid mode). - */ - private var displayMode: MenuItem? = null - /** * Search item. */ @@ -144,7 +140,8 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold catalogue_list.adapter = adapter catalogue_list.layoutManager = llm catalogue_list.addOnScrollListener(listScrollListener) - catalogue_list.addItemDecoration(DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable))) + catalogue_list.addItemDecoration( + DividerItemDecoration(context.theme.getResourceDrawable(R.attr.divider_drawable))) if (presenter.isListMode) { switcher.showNext() @@ -166,28 +163,25 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold android.R.layout.simple_spinner_item, presenter.sources) spinnerAdapter.setDropDownViewResource(R.layout.spinner_item) - val onItemSelected = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { - val source = spinnerAdapter.getItem(position) - if (!presenter.isValidSource(source)) { - spinner.setSelection(selectedIndex) - context.toast(R.string.source_requires_login) - } else if (source != presenter.source) { - selectedIndex = position - showProgressBar() - glm.scrollToPositionWithOffset(0, 0) - llm.scrollToPositionWithOffset(0, 0) - presenter.setActiveSource(source) - } - } - - override fun onNothingSelected(parent: AdapterView<*>) { + val onItemSelected = IgnoreFirstSpinnerListener { position -> + val source = spinnerAdapter.getItem(position) + if (!presenter.isValidSource(source)) { + spinner.setSelection(selectedIndex) + context.toast(R.string.source_requires_login) + } else if (source != presenter.source) { + selectedIndex = position + showProgressBar() + glm.scrollToPositionWithOffset(0, 0) + llm.scrollToPositionWithOffset(0, 0) + presenter.setActiveSource(source) + activity.invalidateOptionsMenu() } } + selectedIndex = presenter.sources.indexOf(presenter.source) + spinner = Spinner(themedContext).apply { adapter = spinnerAdapter - selectedIndex = presenter.sources.indexOf(presenter.source) setSelection(selectedIndex) onItemSelectedListener = onItemSelected } @@ -205,7 +199,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold searchItem = menu.findItem(R.id.action_search).apply { val searchView = actionView as SearchView - if (!query.isNullOrEmpty()) { + if (!query.isBlank()) { expandActionView() searchView.setQuery(query, true) searchView.clearFocus() @@ -223,20 +217,31 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold }) } + // Setup filters button + menu.findItem(R.id.action_set_filter).apply { + if (presenter.source.filters.isEmpty()) { + isEnabled = false + icon.alpha = 128 + } else { + isEnabled = true + icon.alpha = 255 + } + } + // Show next display mode - displayMode = menu.findItem(R.id.action_display_mode).apply { + menu.findItem(R.id.action_display_mode).apply { val icon = if (presenter.isListMode) R.drawable.ic_view_module_white_24dp else R.drawable.ic_view_list_white_24dp setIcon(icon) } - } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.action_display_mode -> swapDisplayMode() + R.id.action_set_filter -> showFiltersDialog() else -> return super.onOptionsItemSelected(item) } return true @@ -312,7 +317,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold */ fun onAddPage(page: Int, mangas: List) { hideProgressBar() - if (page == 0) { + if (page == 1) { adapter.clear() gridScrollListener.resetScroll() listScrollListener.resetScroll() @@ -329,10 +334,10 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold hideProgressBar() Timber.e(error, error.message) - catalogue_view.snack(error.message ?: "") { + catalogue_view.snack(error.message ?: "", Snackbar.LENGTH_INDEFINITE) { setAction(R.string.action_retry) { showProgressBar() - presenter.retryPage() + presenter.requestNext() } } } @@ -352,11 +357,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold fun swapDisplayMode() { presenter.swapDisplayMode() val isListMode = presenter.isListMode - val icon = if (isListMode) - R.drawable.ic_view_module_white_24dp - else - R.drawable.ic_view_list_white_24dp - displayMode?.setIcon(icon) + activity.invalidateOptionsMenu() switcher.showNext() if (!isListMode) { // Initialize mangas if going to grid view @@ -444,4 +445,27 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold }.show() } + /** + * Show the filter dialog for the source. + */ + private fun showFiltersDialog() { + val allFilters = presenter.source.filters + val selectedFilters = presenter.filters + .map { filter -> allFilters.indexOf(filter) } + .toTypedArray() + + MaterialDialog.Builder(context) + .title(R.string.action_set_filter) + .items(allFilters.map { it.name }) + .itemsCallbackMultiChoice(selectedFilters) { dialog, positions, text -> + val newFilters = positions.map { allFilters[it] } + showProgressBar() + presenter.setSourceFilter(newFilters) + true + } + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .show() + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt new file mode 100644 index 000000000..a10243a85 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.ui.catalogue + +import eu.kanade.tachiyomi.data.source.model.MangasPage +import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter +import rx.Observable +import rx.subjects.PublishSubject + +class CataloguePager(val source: OnlineSource, val query: String, val filters: List) { + + private var lastPage: MangasPage? = null + + private val results = PublishSubject.create() + + fun results(): Observable { + return results.asObservable() + } + + fun requestNext(transformer: (Observable) -> Observable): Observable { + val lastPage = lastPage + + val page = if (lastPage == null) + MangasPage(1) + else + MangasPage(lastPage.page + 1).apply { url = lastPage.nextPageUrl!! } + + val observable = if (query.isBlank() && filters.isEmpty()) + source.fetchPopularManga(page) + else + source.fetchSearchManga(page, query, filters) + + return transformer(observable) + .doOnNext { results.onNext(it) } + .doOnNext { this@CataloguePager.lastPage = it } + } + + fun hasNextPage(): Boolean { + return lastPage == null || lastPage?.nextPageUrl != null + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index 6d1b9426a..e4818f12a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -12,9 +12,10 @@ import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.online.LoginSource import eu.kanade.tachiyomi.data.source.online.OnlineSource +import eu.kanade.tachiyomi.data.source.online.OnlineSource.Filter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.RxPager import rx.Observable +import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import rx.subjects.PublishSubject @@ -64,14 +65,14 @@ class CataloguePresenter : BasePresenter() { private set /** - * Pager containing a list of manga results. + * Active filters. */ - private var pager = RxPager() + var filters: List = emptyList() /** - * Last fetched page from network. + * Pager containing a list of manga results. */ - private var lastMangasPage: MangasPage? = null + private lateinit var pager: CataloguePager /** * Subject that initializes a list of manga. @@ -84,27 +85,20 @@ class CataloguePresenter : BasePresenter() { var isListMode: Boolean = false private set - companion object { - /** - * Id of the restartable that delivers a list of manga. - */ - const val PAGER = 1 + /** + * Subscription for the pager. + */ + private var pagerSubscription: Subscription? = null - /** - * Id of the restartable that requests a page of manga from network. - */ - const val REQUEST_PAGE = 2 + /** + * Subscription for one request from the pager. + */ + private var pageSubscription: Subscription? = null - /** - * Id of the restartable that initializes the details of manga. - */ - const val GET_MANGA_DETAILS = 3 - - /** - * Key to save and restore [query] from a [Bundle]. - */ - const val QUERY_KEY = "query_key" - } + /** + * Subscription to initialize manga details. + */ + private var initializerSubscription: Subscription? = null override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) @@ -112,52 +106,68 @@ class CataloguePresenter : BasePresenter() { source = getLastUsedSource() if (savedState != null) { - query = savedState.getString(QUERY_KEY, "") + query = savedState.getString(CataloguePresenter::query.name, "") } - startableLatestCache(GET_MANGA_DETAILS, - { mangaDetailSubject.observeOn(Schedulers.io()) - .flatMap { Observable.from(it) } - .filter { !it.initialized } - .concatMap { getMangaDetailsObservable(it) } - .onBackpressureBuffer() - .observeOn(AndroidSchedulers.mainThread()) }, - { view, manga -> view.onMangaInitialized(manga) }, - { view, error -> Timber.e(error.message) }) - add(prefs.catalogueAsList().asObservable() .subscribe { setDisplayMode(it) }) - startableReplay(PAGER, - { pager.results() }, - { view, pair -> view.onAddPage(pair.first, pair.second) }) - - startableFirst(REQUEST_PAGE, - { pager.request { page -> getMangasPageObservable(page + 1) } }, - { view, next -> }, - { view, error -> view.onAddPageError(error) }) - - start(PAGER) - start(REQUEST_PAGE) + restartPager() } override fun onSave(state: Bundle) { - state.putString(QUERY_KEY, query) + state.putString(CataloguePresenter::query.name, query) super.onSave(state) } /** - * Sets the display mode. + * Restarts the pager for the active source with the provided query and filters. * - * @param asList whether the current mode is in list or not. + * @param query the query. + * @param filters the list of active filters (for search mode). */ - private fun setDisplayMode(asList: Boolean) { - isListMode = asList - if (asList) { - stop(GET_MANGA_DETAILS) - } else { - start(GET_MANGA_DETAILS) + fun restartPager(query: String = this.query, filters: List = this.filters) { + this.query = query + this.filters = filters + + if (!isListMode) { + subscribeToMangaInitializer() } + + // Create a new pager. + pager = CataloguePager(source, query, filters) + + // Prepare the pager. + pagerSubscription?.let { remove(it) } + pagerSubscription = pager.results() + .subscribeReplay({ view, page -> + view.onAddPage(page.page, page.mangas) + }, { view, error -> + Timber.e(error, error.message) + }) + + // Request first page. + requestNext() + } + + /** + * Requests the next page for the active pager. + */ + fun requestNext() { + if (!hasNextPage()) return + + pageSubscription?.let { remove(it) } + pageSubscription = pager.requestNext { getPageTransformer(it) } + .subscribeFirst({ view, page -> + // Nothing to do when onNext is emitted. + }, CatalogueFragment::onAddPageError) + } + + /** + * Returns true if the last fetched page has a next page. + */ + fun hasNextPage(): Boolean { + return pager.hasNextPage() } /** @@ -168,73 +178,64 @@ class CataloguePresenter : BasePresenter() { fun setActiveSource(source: OnlineSource) { prefs.lastUsedCatalogueSource().set(source.id) this.source = source - restartPager() + + restartPager(query = "", filters = emptyList()) } /** - * Restarts the request for the active source. + * Sets the display mode. * - * @param query the query, or null if searching popular manga. + * @param asList whether the current mode is in list or not. */ - fun restartPager(query: String = "") { - this.query = query - stop(REQUEST_PAGE) - lastMangasPage = null - - if (!isListMode) { - start(GET_MANGA_DETAILS) - } - start(PAGER) - start(REQUEST_PAGE) - } - - /** - * Requests the next page for the active pager. - */ - fun requestNext() { - if (hasNextPage()) { - start(REQUEST_PAGE) + private fun setDisplayMode(asList: Boolean) { + isListMode = asList + if (asList) { + initializerSubscription?.let { remove(it) } + } else { + subscribeToMangaInitializer() } } /** - * Returns true if the last fetched page has a next page. + * Subscribes to the initializer of manga details and updates the view if needed. */ - fun hasNextPage(): Boolean { - return lastMangasPage?.nextPageUrl != null - } - - /** - * Retries the current request that failed. - */ - fun retryPage() { - start(REQUEST_PAGE) - } - - /** - * Returns the observable of the network request for a page. - * - * @param page the page number to request. - * @return an observable of the network request. - */ - private fun getMangasPageObservable(page: Int): Observable> { - val nextMangasPage = MangasPage(page) - if (page != 1) { - nextMangasPage.url = lastMangasPage!!.nextPageUrl!! - } - - val observable = if (query.isEmpty()) - source.fetchPopularManga(nextMangasPage) - else - source.fetchSearchManga(nextMangasPage, query) - - return observable.subscribeOn(Schedulers.io()) - .doOnNext { lastMangasPage = it } - .flatMap { Observable.from(it.mangas) } - .map { networkToLocalManga(it) } - .toList() - .doOnNext { initializeMangas(it) } + private fun subscribeToMangaInitializer() { + initializerSubscription?.let { remove(it) } + initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io()) + .flatMap { Observable.from(it) } + .filter { !it.initialized } + .concatMap { getMangaDetailsObservable(it) } + .onBackpressureBuffer() .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ manga -> + @Suppress("DEPRECATION") + view?.onMangaInitialized(manga) + }, { error -> + Timber.e(error, error.message) + }) + .apply { add(this) } + } + + /** + * Returns the function to apply to the observable of the list of manga from the source. + * + * @param observable the observable from the source. + * @return the function to apply. + */ + fun getPageTransformer(observable: Observable): Observable { + return observable.subscribeOn(Schedulers.io()) + .doOnNext { it.mangas.replace { networkToLocalManga(it) } } + .doOnNext { initializeMangas(it.mangas) } + .observeOn(AndroidSchedulers.mainThread()) + } + + /** + * Replaces an object in the list with another. + */ + fun MutableList.replace(block: (T) -> T) { + forEachIndexed { i, obj -> + set(i, block(obj)) + } } /** @@ -354,4 +355,13 @@ class CataloguePresenter : BasePresenter() { prefs.catalogueAsList().set(!isListMode) } + /** + * Set the active filters for the current source. + * + * @param selectedFilters a list of active filters. + */ + fun setSourceFilter(selectedFilters: List) { + restartPager(filters = selectedFilters) + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt b/app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt deleted file mode 100644 index c46d68e1a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/util/RxPager.kt +++ /dev/null @@ -1,21 +0,0 @@ -package eu.kanade.tachiyomi.util - -import android.util.Pair -import rx.Observable -import rx.subjects.PublishSubject - -class RxPager { - - private val results = PublishSubject.create>() - private var requestedCount: Int = 0 - - fun results(): Observable>> { - requestedCount = 0 - return results.map { Pair(requestedCount++, it) } - } - - fun request(networkObservable: (Int) -> Observable>) = - networkObservable(requestedCount).doOnNext { results.onNext(it) } - -} - diff --git a/app/src/main/res/layout/fragment_catalogue.xml b/app/src/main/res/layout/fragment_catalogue.xml index 1f5ec2e4e..d862a45b7 100644 --- a/app/src/main/res/layout/fragment_catalogue.xml +++ b/app/src/main/res/layout/fragment_catalogue.xml @@ -1,48 +1,55 @@ - + - + - - - + android:layout_gravity="center_vertical|center_horizontal" + android:visibility="gone"/> - + android:layout_height="0dp" + android:layout_weight="1"> + - + - + - + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/catalogue_list.xml b/app/src/main/res/menu/catalogue_list.xml index 369402eea..ff4373df4 100644 --- a/app/src/main/res/menu/catalogue_list.xml +++ b/app/src/main/res/menu/catalogue_list.xml @@ -9,6 +9,12 @@ app:showAsAction="collapseActionView|ifRoom" app:actionViewClass="android.support.v7.widget.SearchView"/> + + Resume Open in browser Change display mode + Set filter Cancel Sort Install