Fixed some crashes in the catalogue and the reader

This commit is contained in:
len 2016-04-27 00:23:06 +02:00
parent 4b7159648a
commit de6cc8394e
4 changed files with 138 additions and 173 deletions

View File

@ -33,7 +33,7 @@ import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit.MILLISECONDS
/** /**
* Fragment that shows the manga from the catalogue. * Fragment that shows the manga from the catalogue.
@ -65,7 +65,8 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
/** /**
* Query of the search box. * Query of the search box.
*/ */
private var query = "" private val query: String?
get() = presenter.query
/** /**
* Selected index of the spinner (selected source). * Selected index of the spinner (selected source).
@ -109,23 +110,11 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
get() = (activity as MainActivity).toolbar get() = (activity as MainActivity).toolbar
companion object { companion object {
/**
* Key to save and restore [query] from a [Bundle].
*/
const val QUERY_KEY = "query_key"
/**
* Key to save and restore [selectedIndex] from a [Bundle].
*/
const val SELECTED_INDEX_KEY = "selected_index_key"
/** /**
* Creates a new instance of this fragment. * Creates a new instance of this fragment.
* *
* @return a new instance of [CatalogueFragment]. * @return a new instance of [CatalogueFragment].
*/ */
@JvmStatic
fun newInstance(): CatalogueFragment { fun newInstance(): CatalogueFragment {
return CatalogueFragment() return CatalogueFragment()
} }
@ -134,13 +123,6 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
if (savedState != null) {
selectedIndex = savedState.getInt(SELECTED_INDEX_KEY)
query = savedState.getString(QUERY_KEY)
} else {
selectedIndex = presenter.getLastUsedSourceIndex()
}
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
@ -188,19 +170,15 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
val onItemSelected = object : AdapterView.OnItemSelectedListener { val onItemSelected = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
val source = spinnerAdapter.getItem(position) val source = spinnerAdapter.getItem(position)
if (selectedIndex != position || adapter.isEmpty) { if (!presenter.isValidSource(source)) {
// Set previous selection if it's not a valid source and notify the user spinner.setSelection(selectedIndex)
if (!presenter.isValidSource(source)) { context.toast(R.string.source_requires_login)
spinner.setSelection(presenter.findFirstValidSource()) } else if (source != presenter.source) {
context.toast(R.string.source_requires_login) selectedIndex = position
} else { showProgressBar()
selectedIndex = position glm.scrollToPositionWithOffset(0, 0)
presenter.setEnabledSource(selectedIndex) llm.scrollToPositionWithOffset(0, 0)
showProgressBar() presenter.setActiveSource(source)
glm.scrollToPositionWithOffset(0, 0)
llm.scrollToPositionWithOffset(0, 0)
presenter.startRequesting(source)
}
} }
} }
@ -210,18 +188,15 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
spinner = Spinner(themedContext).apply { spinner = Spinner(themedContext).apply {
adapter = spinnerAdapter adapter = spinnerAdapter
selectedIndex = presenter.sources.indexOf(presenter.source)
setSelection(selectedIndex) setSelection(selectedIndex)
onItemSelectedListener = onItemSelected onItemSelectedListener = onItemSelected
} }
setToolbarTitle("") setToolbarTitle("")
toolbar.addView(spinner) toolbar.addView(spinner)
}
override fun onSaveInstanceState(outState: Bundle) { showProgressBar()
outState.putInt(SELECTED_INDEX_KEY, selectedIndex)
outState.putString(QUERY_KEY, query)
super.onSaveInstanceState(outState)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -268,14 +243,16 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
return true return true
} }
override fun onStart() { override fun onResume() {
super.onStart() super.onResume()
initializeSearchSubscription() queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { searchWithQuery(it) }
} }
override fun onStop() { override fun onPause() {
destroySearchSubscription() queryDebouncerSubscription?.unsubscribe()
super.onStop() super.onPause()
} }
override fun onDestroyView() { override fun onDestroyView() {
@ -288,51 +265,34 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
} }
/** /**
* Listen for query events on the debouncer. * Called when the input text changes or is submitted.
*/
private fun initializeSearchSubscription() {
queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { restartRequest(it) }
}
/**
* Unsubscribe from the query debouncer.
*/
private fun destroySearchSubscription() {
queryDebouncerSubscription?.unsubscribe()
}
/**
* Called when the input text changes or is submitted
* *
* @param query the new query. * @param query the new query.
* @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT]. * @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
*/ */
private fun onSearchEvent(query: String, now: Boolean) { private fun onSearchEvent(query: String, now: Boolean) {
if (now) { if (now) {
restartRequest(query) searchWithQuery(query)
} else { } else {
queryDebouncerSubject.onNext(query) queryDebouncerSubject.onNext(query)
} }
} }
/** /**
* Restarts the request. * Restarts the request with a new query.
* *
* @param newQuery the new query. * @param newQuery the new query.
*/ */
private fun restartRequest(newQuery: String) { private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing // If text didn't change, do nothing
if (query == newQuery) if (query == newQuery)
return return
query = newQuery
showProgressBar() showProgressBar()
catalogue_grid.layoutManager.scrollToPosition(0) catalogue_grid.layoutManager.scrollToPosition(0)
catalogue_list.layoutManager.scrollToPosition(0) catalogue_list.layoutManager.scrollToPosition(0)
presenter.restartRequest(query) presenter.restartPager(newQuery)
} }
/** /**
@ -373,7 +333,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
catalogue_view.snack(error.message ?: "") { catalogue_view.snack(error.message ?: "") {
setAction(R.string.action_retry) { setAction(R.string.action_retry) {
showProgressBar() showProgressBar()
presenter.retryRequest() presenter.retryPage()
} }
} }
} }
@ -469,16 +429,16 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
* @param position the position of the element clicked. * @param position the position of the element clicked.
*/ */
override fun onListItemLongClick(position: Int) { override fun onListItemLongClick(position: Int) {
val selectedManga = adapter.getItem(position) val manga = adapter.getItem(position) ?: return
val textRes = if (selectedManga.favorite) R.string.remove_from_library else R.string.add_to_library val textRes = if (manga.favorite) R.string.remove_from_library else R.string.add_to_library
MaterialDialog.Builder(activity) MaterialDialog.Builder(activity)
.items(getString(textRes)) .items(getString(textRes))
.itemsCallback { dialog, itemView, which, text -> .itemsCallback { dialog, itemView, which, text ->
when (which) { when (which) {
0 -> { 0 -> {
presenter.changeMangaFavorite(selectedManga) presenter.changeMangaFavorite(manga)
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
} }
} }

View File

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.EN
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.base.Source import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.source.model.MangasPage import eu.kanade.tachiyomi.data.source.model.MangasPage
@ -51,12 +52,13 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
/** /**
* Query from the view. * Query from the view.
*/ */
private var query: String? = null var query: String? = null
private set
/** /**
* Pager containing a list of manga results. * Pager containing a list of manga results.
*/ */
private lateinit var pager: RxPager<Manga> private var pager = RxPager<Manga>()
/** /**
* Last fetched page from network. * Last fetched page from network.
@ -76,45 +78,36 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
companion object { companion object {
/** /**
* Id of the restartable that delivers a list of manga from network. * Id of the restartable that delivers a list of manga.
*/ */
const val GET_MANGA_LIST = 1 const val PAGER = 1
/** /**
* Id of the restartable that requests the list of manga from network. * Id of the restartable that requests a page of manga from network.
*/ */
const val GET_MANGA_PAGE = 2 const val REQUEST_PAGE = 2
/** /**
* Id of the restartable that initializes the details of a manga. * Id of the restartable that initializes the details of manga.
*/ */
const val GET_MANGA_DETAIL = 3 const val GET_MANGA_DETAILS = 3
/** /**
* Key to save and restore [source] from a [Bundle]. * Key to save and restore [query] from a [Bundle].
*/ */
const val ACTIVE_SOURCE_KEY = "active_source" const val QUERY_KEY = "query_key"
} }
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
source = getLastUsedSource()
if (savedState != null) { if (savedState != null) {
source = sourceManager.get(savedState.getInt(ACTIVE_SOURCE_KEY))!! query = savedState.getString(QUERY_KEY)
} }
pager = RxPager() startableLatestCache(GET_MANGA_DETAILS,
startableReplay(GET_MANGA_LIST,
{ pager.results() },
{ view, pair -> view.onAddPage(pair.first, pair.second) })
startableFirst(GET_MANGA_PAGE,
{ pager.request { page -> getMangasPageObservable(page + 1) } },
{ view, next -> },
{ view, error -> view.onAddPageError(error) })
startableLatestCache(GET_MANGA_DETAIL,
{ mangaDetailSubject.observeOn(Schedulers.io()) { mangaDetailSubject.observeOn(Schedulers.io())
.flatMap { Observable.from(it) } .flatMap { Observable.from(it) }
.filter { !it.initialized } .filter { !it.initialized }
@ -126,10 +119,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
add(prefs.catalogueAsList().asObservable() add(prefs.catalogueAsList().asObservable()
.subscribe { setDisplayMode(it) }) .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)
} }
override fun onSave(state: Bundle) { override fun onSave(state: Bundle) {
state.putInt(ACTIVE_SOURCE_KEY, source.id) state.putString(QUERY_KEY, query)
super.onSave(state) super.onSave(state)
} }
@ -141,37 +146,38 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
private fun setDisplayMode(asList: Boolean) { private fun setDisplayMode(asList: Boolean) {
isListMode = asList isListMode = asList
if (asList) { if (asList) {
stop(GET_MANGA_DETAIL) stop(GET_MANGA_DETAILS)
} else { } else {
start(GET_MANGA_DETAIL) start(GET_MANGA_DETAILS)
} }
} }
/** /**
* Starts the request with the given source. * Sets the active source and restarts the pager.
* *
* @param source the active source. * @param source the new active source.
*/ */
fun startRequesting(source: Source) { fun setActiveSource(source: Source) {
prefs.lastUsedCatalogueSource().set(source.id)
this.source = source this.source = source
restartRequest(null) restartPager(null)
} }
/** /**
* Restarts the request for the active source with a query. * Restarts the request for the active source.
* *
* @param query a query, or null if searching popular manga. * @param query the query, or null if searching popular manga.
*/ */
fun restartRequest(query: String?) { fun restartPager(query: String?) {
this.query = query this.query = query
stop(GET_MANGA_PAGE) stop(REQUEST_PAGE)
lastMangasPage = null lastMangasPage = null
if (!isListMode) { if (!isListMode) {
start(GET_MANGA_DETAIL) start(GET_MANGA_DETAILS)
} }
start(GET_MANGA_LIST) start(PAGER)
start(GET_MANGA_PAGE) start(REQUEST_PAGE)
} }
/** /**
@ -179,15 +185,22 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
*/ */
fun requestNext() { fun requestNext() {
if (hasNextPage()) { if (hasNextPage()) {
start(GET_MANGA_PAGE) start(REQUEST_PAGE)
} }
} }
/** /**
* Retry a failed request. * Returns true if the last fetched page has a next page.
*/ */
fun retryRequest() { fun hasNextPage(): Boolean {
start(GET_MANGA_PAGE) return lastMangasPage?.nextPageUrl != null
}
/**
* Retries the current request that failed.
*/
fun retryPage() {
start(REQUEST_PAGE)
} }
/** /**
@ -202,12 +215,12 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
nextMangasPage.url = lastMangasPage!!.nextPageUrl nextMangasPage.url = lastMangasPage!!.nextPageUrl
} }
val obs = if (query.isNullOrEmpty()) val observable = if (query.isNullOrEmpty())
source.pullPopularMangasFromNetwork(nextMangasPage) source.pullPopularMangasFromNetwork(nextMangasPage)
else else
source.searchMangasFromNetwork(nextMangasPage, query!!) source.searchMangasFromNetwork(nextMangasPage, query!!)
return obs.subscribeOn(Schedulers.io()) return observable.subscribeOn(Schedulers.io())
.doOnNext { lastMangasPage = it } .doOnNext { lastMangasPage = it }
.flatMap { Observable.from(it.mangas) } .flatMap { Observable.from(it.mangas) }
.map { networkToLocalManga(it) } .map { networkToLocalManga(it) }
@ -259,23 +272,17 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
} }
/** /**
* Returns true if the last fetched page has a next page. * Returns the last used source from preferences or the first valid source.
*/
fun hasNextPage(): Boolean {
return lastMangasPage?.nextPageUrl != null
}
/**
* Gets the last used source from preferences, or the first valid source.
* *
* @return the index of the last used source. * @return a source.
*/ */
fun getLastUsedSourceIndex(): Int { fun getLastUsedSource(): Source {
val index = prefs.lastUsedCatalogueSource().get() ?: -1 val id = prefs.lastUsedCatalogueSource().get() ?: -1
if (index < 0 || index >= sources.size || !isValidSource(sources[index])) { val source = sourceManager.get(id)
if (!isValidSource(source)) {
return findFirstValidSource() return findFirstValidSource()
} }
return index return source!!
} }
/** /**
@ -284,11 +291,16 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* @param source the source to check. * @param source the source to check.
* @return true if the source is valid, false otherwise. * @return true if the source is valid, false otherwise.
*/ */
fun isValidSource(source: Source): Boolean = with(source) { fun isValidSource(source: Source?): Boolean {
if (!isLoginRequired || isLogged) if (source == null) return false
return true
prefs.sourceUsername(this) != "" && prefs.sourcePassword(this) != "" return with(source) {
if (!isLoginRequired || isLogged) {
true
} else {
prefs.sourceUsername(this) != "" && prefs.sourcePassword(this) != ""
}
}
} }
/** /**
@ -296,17 +308,8 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
* *
* @return the index of the first valid source. * @return the index of the first valid source.
*/ */
fun findFirstValidSource(): Int { fun findFirstValidSource(): Source {
return sources.indexOfFirst { isValidSource(it) } return sources.find { isValidSource(it) }!!
}
/**
* Sets the enabled source.
*
* @param index the index of the source in [sources].
*/
fun setEnabledSource(index: Int) {
prefs.lastUsedCatalogueSource().set(index)
} }
/** /**
@ -317,7 +320,7 @@ class CataloguePresenter : BasePresenter<CatalogueFragment>() {
// Ensure at least one language // Ensure at least one language
if (languages.isEmpty()) { if (languages.isEmpty()) {
languages.add("EN") languages.add(EN.code)
} }
return sourceManager.getSources() return sourceManager.getSources()

View File

@ -98,38 +98,40 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
val coverCache = presenter.coverCache val coverCache = presenter.coverCache
val headers = presenter.source.glideHeaders val headers = presenter.source.glideHeaders
// Check if thumbnail_url is given. // Set cover if it wasn't already.
manga.thumbnail_url?.let { url -> if (manga_cover.drawable == null) {
if (manga.favorite) { manga.thumbnail_url?.let { url ->
coverCache.saveOrLoadFromCache(url, headers) { if (manga.favorite) {
if (isResumed) { coverCache.saveOrLoadFromCache(url, headers) {
Glide.with(context) if (isResumed) {
.load(it) Glide.with(context)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .load(it)
.centerCrop() .diskCacheStrategy(DiskCacheStrategy.RESULT)
.signature(StringSignature(it.lastModified().toString())) .centerCrop()
.into(manga_cover) .signature(StringSignature(it.lastModified().toString()))
.into(manga_cover)
Glide.with(context) Glide.with(context)
.load(it) .load(it)
.diskCacheStrategy(DiskCacheStrategy.RESULT) .diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop() .centerCrop()
.signature(StringSignature(it.lastModified().toString())) .signature(StringSignature(it.lastModified().toString()))
.into(backdrop) .into(backdrop)
}
} }
} } else {
} else { Glide.with(context)
Glide.with(context) .load(if (headers != null) GlideUrl(url, headers) else url)
.load(if (headers != null) GlideUrl(url, headers) else url) .diskCacheStrategy(DiskCacheStrategy.SOURCE)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .centerCrop()
.centerCrop() .into(manga_cover)
.into(manga_cover)
Glide.with(context) Glide.with(context)
.load(if (headers != null) GlideUrl(url, headers) else url) .load(if (headers != null) GlideUrl(url, headers) else url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE) .diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop() .centerCrop()
.into(backdrop) .into(backdrop)
}
} }
} }
} }

View File

@ -78,7 +78,7 @@ abstract class BaseReader : BaseFragment() {
* Returns the active page. * Returns the active page.
*/ */
fun getActivePage(): Page { fun getActivePage(): Page {
return pages[currentPage] return pages.getOrElse(currentPage) { pages[0] }
} }
/** /**