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
This commit is contained in:
parent
4171e87b4b
commit
2fb3b50535
@ -47,5 +47,4 @@ interface Source {
|
||||
* @param page the page.
|
||||
*/
|
||||
fun fetchImage(page: Page): Observable<Page>
|
||||
|
||||
}
|
@ -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<MangasPage> = client
|
||||
.newCall(searchMangaRequest(page, query))
|
||||
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = 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<Filter>): 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<Filter>): 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<Filter>)
|
||||
|
||||
/**
|
||||
* 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<Filter> = emptyList()
|
||||
}
|
||||
|
@ -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<Filter>) {
|
||||
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
|
||||
|
||||
}
|
||||
|
@ -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<Filter>): 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<Filter>) = 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<Filter>) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<Filter>) = "$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<Filter>): String = filters
|
||||
.map {
|
||||
";i" + it.id
|
||||
}.joinToString()
|
||||
|
||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<Filter>): 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<Filter>) {
|
||||
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 {
|
||||
@ -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<Filter> = 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")
|
||||
)
|
||||
}
|
@ -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<Filter>): 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)
|
||||
}
|
||||
|
||||
override fun searchMangaInitialUrl(query: String) = "$baseUrl/AdvanceSearch"
|
||||
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, filters: List<Filter>) = "$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<Page>) {}
|
||||
override fun pageListParse(document: Document, pages: MutableList<Page>) {
|
||||
}
|
||||
|
||||
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<Filter> = 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")
|
||||
)
|
||||
}
|
@ -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<Filter>) =
|
||||
"$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<Filter> = 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")
|
||||
)
|
||||
}
|
@ -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<Filter>) =
|
||||
"$baseUrl/search.php?name=$query&page=1&sort=views&order=za"
|
||||
|
||||
override fun searchMangaSelector() = "div.result_search > dl:has(dt)"
|
||||
|
@ -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<Filter>) =
|
||||
"$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"
|
||||
|
@ -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<Filter>) =
|
||||
"$baseUrl/search"
|
||||
|
||||
|
||||
override fun searchMangaRequest(page: MangasPage, query: String): Request {
|
||||
override fun searchMangaRequest(page: MangasPage, query: String, filters: List<OnlineSource.Filter>): 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())
|
||||
|
@ -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<Filter>) = "$baseUrl/search/?wd=$query"
|
||||
|
||||
override fun searchMangaSelector() = ".searchresult td > div"
|
||||
|
||||
|
@ -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<Filter>) = "$baseUrl/?do=search&subaction=search&story=$query"
|
||||
|
||||
override fun popularMangaSelector() = "div.content_row"
|
||||
|
||||
|
@ -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<Filter>) = "$baseUrl/search?q=$query"
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
|
@ -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<Filter>) = "$baseUrl/search?q=$query"
|
||||
|
||||
override fun popularMangaSelector() = "div.desc"
|
||||
|
||||
|
@ -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<CataloguePresenter>(), FlexibleViewHold
|
||||
/**
|
||||
* Query of the search box.
|
||||
*/
|
||||
private val query: String?
|
||||
private val query: String
|
||||
get() = presenter.query
|
||||
|
||||
/**
|
||||
@ -92,11 +93,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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<CataloguePresenter>(), 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,8 +163,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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 onItemSelected = IgnoreFirstSpinnerListener { position ->
|
||||
val source = spinnerAdapter.getItem(position)
|
||||
if (!presenter.isValidSource(source)) {
|
||||
spinner.setSelection(selectedIndex)
|
||||
@ -178,16 +174,14 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
||||
glm.scrollToPositionWithOffset(0, 0)
|
||||
llm.scrollToPositionWithOffset(0, 0)
|
||||
presenter.setActiveSource(source)
|
||||
activity.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>) {
|
||||
}
|
||||
}
|
||||
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<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), FlexibleViewHold
|
||||
*/
|
||||
fun onAddPage(page: Int, mangas: List<Manga>) {
|
||||
hideProgressBar()
|
||||
if (page == 0) {
|
||||
if (page == 1) {
|
||||
adapter.clear()
|
||||
gridScrollListener.resetScroll()
|
||||
listScrollListener.resetScroll()
|
||||
@ -329,10 +334,10 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), 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<CataloguePresenter>(), 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<CataloguePresenter>(), 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()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<Filter>) {
|
||||
|
||||
private var lastPage: MangasPage? = null
|
||||
|
||||
private val results = PublishSubject.create<MangasPage>()
|
||||
|
||||
fun results(): Observable<MangasPage> {
|
||||
return results.asObservable()
|
||||
}
|
||||
|
||||
fun requestNext(transformer: (Observable<MangasPage>) -> Observable<MangasPage>): Observable<MangasPage> {
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@ -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<CatalogueFragment>() {
|
||||
private set
|
||||
|
||||
/**
|
||||
* Pager containing a list of manga results.
|
||||
* Active filters.
|
||||
*/
|
||||
private var pager = RxPager<Manga>()
|
||||
var filters: List<Filter> = 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<CatalogueFragment>() {
|
||||
var isListMode: Boolean = false
|
||||
private set
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Id of the restartable that delivers a list of manga.
|
||||
* Subscription for the pager.
|
||||
*/
|
||||
const val PAGER = 1
|
||||
private var pagerSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Id of the restartable that requests a page of manga from network.
|
||||
* Subscription for one request from the pager.
|
||||
*/
|
||||
const val REQUEST_PAGE = 2
|
||||
private var pageSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Id of the restartable that initializes the details of manga.
|
||||
* Subscription to initialize manga details.
|
||||
*/
|
||||
const val GET_MANGA_DETAILS = 3
|
||||
|
||||
/**
|
||||
* Key to save and restore [query] from a [Bundle].
|
||||
*/
|
||||
const val QUERY_KEY = "query_key"
|
||||
}
|
||||
private var initializerSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
@ -112,52 +106,68 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
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<Filter> = 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<CatalogueFragment>() {
|
||||
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<List<Manga>> {
|
||||
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<MangasPage>): Observable<MangasPage> {
|
||||
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 <T> MutableList<T>.replace(block: (T) -> T) {
|
||||
forEachIndexed { i, obj ->
|
||||
set(i, block(obj))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -354,4 +355,13 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||
prefs.catalogueAsList().set(!isListMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active filters for the current source.
|
||||
*
|
||||
* @param selectedFilters a list of active filters.
|
||||
*/
|
||||
fun setSourceFilter(selectedFilters: List<Filter>) {
|
||||
restartPager(filters = selectedFilters)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.util.Pair
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
|
||||
class RxPager<T> {
|
||||
|
||||
private val results = PublishSubject.create<List<T>>()
|
||||
private var requestedCount: Int = 0
|
||||
|
||||
fun results(): Observable<Pair<Int, List<T>>> {
|
||||
requestedCount = 0
|
||||
return results.map { Pair(requestedCount++, it) }
|
||||
}
|
||||
|
||||
fun request(networkObservable: (Int) -> Observable<List<T>>) =
|
||||
networkObservable(requestedCount).doOnNext { results.onNext(it) }
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
@ -45,4 +50,6 @@
|
||||
android:layout_gravity="center_vertical|center_horizontal"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
@ -9,6 +9,12 @@
|
||||
app:showAsAction="collapseActionView|ifRoom"
|
||||
app:actionViewClass="android.support.v7.widget.SearchView"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_set_filter"
|
||||
android:title="@string/action_set_filter"
|
||||
android:icon="@drawable/ic_filter_list_white_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_display_mode"
|
||||
android:title="@string/action_display_mode"
|
||||
|
@ -51,6 +51,7 @@
|
||||
<string name="action_resume">Resume</string>
|
||||
<string name="action_open_in_browser">Open in browser</string>
|
||||
<string name="action_display_mode">Change display mode</string>
|
||||
<string name="action_set_filter">Set filter</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
<string name="action_sort">Sort</string>
|
||||
<string name="action_install">Install</string>
|
||||
|
Loading…
Reference in New Issue
Block a user