mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-28 09:15:12 +03:00
Repackage catalogue to match the UI
This commit is contained in:
parent
d690d6e0e3
commit
297fed6aef
32 changed files with 1385 additions and 1386 deletions
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
|
@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.util.getResourceColor
|
|||
/**
|
||||
* Adapter that holds the catalogue cards.
|
||||
*
|
||||
* @param controller instance of [CatalogueMainController].
|
||||
* @param controller instance of [CatalogueController].
|
||||
*/
|
||||
class CatalogueMainAdapter(val controller: CatalogueMainController) :
|
||||
class CatalogueAdapter(val controller: CatalogueController) :
|
||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
||||
|
||||
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
|
||||
|
@ -31,7 +31,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
|
|||
|
||||
/**
|
||||
* Listener which should be called when user clicks browse.
|
||||
* Note: Should only be handled by [CatalogueMainController]
|
||||
* Note: Should only be handled by [CatalogueController]
|
||||
*/
|
||||
interface OnBrowseClickListener {
|
||||
fun onBrowseClick(position: Int)
|
||||
|
@ -39,7 +39,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
|
|||
|
||||
/**
|
||||
* Listener which should be called when user clicks latest.
|
||||
* Note: Should only be handled by [CatalogueMainController]
|
||||
* Note: Should only be handled by [CatalogueController]
|
||||
*/
|
||||
interface OnLatestClickListener {
|
||||
fun onLatestClick(position: Int)
|
|
@ -1,520 +1,231 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
import android.support.v7.widget.*
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.view.*
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
|
||||
import kotlinx.android.synthetic.main.catalogue_controller.*
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subscriptions.Subscriptions
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.TimeUnit
|
||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Controller to manage the catalogues available in the app.
|
||||
* This controller shows and manages the different catalogues enabled by the user.
|
||||
* This controller should only handle UI actions, IO actions should be done by [CataloguePresenter]
|
||||
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
|
||||
* [CatalogueAdapter.OnBrowseClickListener] call function data on browse item click.
|
||||
* [CatalogueAdapter.OnLatestClickListener] call function data on latest item click
|
||||
*/
|
||||
open class CatalogueController(bundle: Bundle) :
|
||||
NucleusController<CataloguePresenter>(bundle),
|
||||
SecondaryDrawerController,
|
||||
class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
SourceLoginDialog.Listener,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
ChangeMangaCategoriesDialog.Listener {
|
||||
|
||||
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
})
|
||||
CatalogueAdapter.OnBrowseClickListener,
|
||||
CatalogueAdapter.OnLatestClickListener {
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
* Application preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
/**
|
||||
* Adapter containing the list of manga from the catalogue.
|
||||
* Adapter containing sources.
|
||||
*/
|
||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
private var adapter : CatalogueAdapter? = null
|
||||
|
||||
/**
|
||||
* Snackbar containing an error message when a request fails.
|
||||
* Called when controller is initialized.
|
||||
*/
|
||||
private var snack: Snackbar? = null
|
||||
|
||||
/**
|
||||
* Navigation view containing filter items.
|
||||
*/
|
||||
private var navView: CatalogueNavigationView? = null
|
||||
|
||||
/**
|
||||
* Recycler view with the list of results.
|
||||
*/
|
||||
private var recycler: RecyclerView? = null
|
||||
|
||||
/**
|
||||
* Drawer listener to allow swipe only for closing the drawer.
|
||||
*/
|
||||
private var drawerListener: DrawerLayout.DrawerListener? = null
|
||||
|
||||
/**
|
||||
* Subscription for the search view.
|
||||
*/
|
||||
private var searchViewSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for the number of manga per row.
|
||||
*/
|
||||
private var numColumnsSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Endless loading item.
|
||||
*/
|
||||
private var progressItem: ProgressItem? = null
|
||||
|
||||
init {
|
||||
// Enable the option menu
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of controller.
|
||||
*
|
||||
* @return title.
|
||||
*/
|
||||
override fun getTitle(): String? {
|
||||
return presenter.source.name
|
||||
return applicationContext?.getString(R.string.label_catalogues)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [CataloguePresenter] used in controller.
|
||||
*
|
||||
* @return instance of [CataloguePresenter]
|
||||
*/
|
||||
override fun createPresenter(): CataloguePresenter {
|
||||
return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
|
||||
return CataloguePresenter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the view with [R.layout.catalogue_main_controller].
|
||||
*
|
||||
* @param inflater used to load the layout xml.
|
||||
* @param container containing parent views.
|
||||
* @return inflated view.
|
||||
*/
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.catalogue_controller, container, false)
|
||||
return inflater.inflate(R.layout.catalogue_main_controller, container, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
*
|
||||
* @param view view of controller
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Initialize adapter, scroll listener and recycler views
|
||||
adapter = FlexibleAdapter(null, this)
|
||||
setupRecycler(view)
|
||||
adapter = CatalogueAdapter(this)
|
||||
|
||||
navView?.setFilters(presenter.filterItems)
|
||||
|
||||
progress?.visible()
|
||||
// Create recycler and set adapter.
|
||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
recycler.adapter = adapter
|
||||
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
numColumnsSubscription = null
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = null
|
||||
adapter = null
|
||||
snack = null
|
||||
recycler = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
|
||||
// Inflate and prepare drawer
|
||||
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
|
||||
this.navView = navView
|
||||
drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
|
||||
drawer.addDrawerListener(it)
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
|
||||
presenter.updateSources()
|
||||
}
|
||||
navView.setFilters(presenter.filterItems)
|
||||
}
|
||||
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
|
||||
|
||||
navView.onSearchClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
showProgressBar()
|
||||
/**
|
||||
* Called when login dialog is closed, refreshes the adapter.
|
||||
*
|
||||
* @param source clicked item containing source information.
|
||||
*/
|
||||
override fun loginDialogClosed(source: LoginSource) {
|
||||
if (source.isLogged()) {
|
||||
adapter?.clear()
|
||||
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
|
||||
presenter.loadSources()
|
||||
}
|
||||
|
||||
navView.onResetClicked = {
|
||||
presenter.appliedFilters = FilterList()
|
||||
val newFilters = presenter.source.getFilterList()
|
||||
presenter.sourceFilters = newFilters
|
||||
navView.setFilters(presenter.filterItems)
|
||||
}
|
||||
return navView
|
||||
}
|
||||
|
||||
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
|
||||
drawerListener?.let { drawer.removeDrawerListener(it) }
|
||||
drawerListener = null
|
||||
navView = null
|
||||
}
|
||||
|
||||
private fun setupRecycler(view: View) {
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
|
||||
var oldPosition = RecyclerView.NO_POSITION
|
||||
val oldRecycler = catalogue_view?.getChildAt(1)
|
||||
if (oldRecycler is RecyclerView) {
|
||||
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
oldRecycler.adapter = null
|
||||
|
||||
catalogue_view?.removeView(oldRecycler)
|
||||
}
|
||||
|
||||
val recycler = if (presenter.isListMode) {
|
||||
RecyclerView(view.context).apply {
|
||||
id = R.id.recycler
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
}
|
||||
/**
|
||||
* Called when item is clicked
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||
val source = item.source
|
||||
if (source is LoginSource && !source.isLogged()) {
|
||||
val dialog = SourceLoginDialog(source)
|
||||
dialog.targetController = this
|
||||
dialog.showDialog(router)
|
||||
} else {
|
||||
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
|
||||
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
|
||||
.doOnNext { spanCount = it }
|
||||
.skip(1)
|
||||
// Set again the adapter to recalculate the covers height
|
||||
.subscribe { adapter = this@CatalogueController.adapter }
|
||||
|
||||
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (adapter?.getItemViewType(position)) {
|
||||
R.layout.catalogue_grid_item, null -> 1
|
||||
else -> spanCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Open the catalogue view.
|
||||
openCatalogue(source, BrowseCatalogueController(source))
|
||||
}
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
|
||||
catalogue_view.addView(recycler, 1)
|
||||
|
||||
if (oldPosition != RecyclerView.NO_POSITION) {
|
||||
recycler.layoutManager.scrollToPosition(oldPosition)
|
||||
}
|
||||
this.recycler = recycler
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when browse is clicked in [CatalogueAdapter]
|
||||
*/
|
||||
override fun onBrowseClick(position: Int) {
|
||||
onItemClick(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when latest is clicked in [CatalogueAdapter]
|
||||
*/
|
||||
override fun onLatestClick(position: Int) {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
openCatalogue(item.source, LatestUpdatesController(item.source))
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a catalogue with the given controller.
|
||||
*/
|
||||
private fun openCatalogue(source: CatalogueSource, controller: BrowseCatalogueController) {
|
||||
preferences.lastUsedCatalogueSource().set(source.id)
|
||||
router.pushController(controller.withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
* @param menu menu containing options.
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.catalogue_list, menu)
|
||||
// Inflate menu
|
||||
inflater.inflate(R.menu.catalogue_main, menu)
|
||||
|
||||
// Initialize search menu
|
||||
menu.findItem(R.id.action_search).apply {
|
||||
val searchView = actionView as SearchView
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
val query = presenter.query
|
||||
if (!query.isBlank()) {
|
||||
expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
}
|
||||
// Change hint to show global search.
|
||||
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
|
||||
|
||||
val searchEventsObservable = searchView.queryTextChangeEvents()
|
||||
.skip(1)
|
||||
.share()
|
||||
val writingObservable = searchEventsObservable
|
||||
.filter { !it.isSubmitted }
|
||||
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
val submitObservable = searchEventsObservable
|
||||
.filter { it.isSubmitted }
|
||||
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
|
||||
.map { it.queryText().toString() }
|
||||
.distinctUntilChanged()
|
||||
.subscribeUntilDestroy { searchWithQuery(it) }
|
||||
|
||||
untilDestroySubscriptions.add(
|
||||
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
|
||||
}
|
||||
|
||||
// Setup filters button
|
||||
menu.findItem(R.id.action_set_filter).apply {
|
||||
icon.mutate()
|
||||
if (presenter.sourceFilters.isEmpty()) {
|
||||
isEnabled = false
|
||||
icon.alpha = 128
|
||||
} else {
|
||||
isEnabled = true
|
||||
icon.alpha = 255
|
||||
}
|
||||
}
|
||||
|
||||
// Show next display mode
|
||||
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)
|
||||
}
|
||||
// Create query listener which opens the global search view.
|
||||
searchView.queryTextChangeEvents()
|
||||
.filter { it.isSubmitted }
|
||||
.subscribeUntilDestroy {
|
||||
val query = it.queryText().toString()
|
||||
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an option menu item has been selected by the user.
|
||||
*
|
||||
* @param item The selected item.
|
||||
* @return True if this event has been consumed, false if it has not.
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_display_mode -> swapDisplayMode()
|
||||
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
|
||||
// Initialize option to open catalogue settings.
|
||||
R.id.action_settings -> {
|
||||
router.pushController((RouterTransaction.with(SettingsSourcesController()))
|
||||
.popChangeHandler(SettingsSourcesFadeChangeHandler())
|
||||
.pushChangeHandler(FadeChangeHandler()))
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the request with a new query.
|
||||
*
|
||||
* @param newQuery the new query.
|
||||
* Called to update adapter containing sources.
|
||||
*/
|
||||
private fun searchWithQuery(newQuery: String) {
|
||||
// If text didn't change, do nothing
|
||||
if (presenter.query == newQuery)
|
||||
return
|
||||
|
||||
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
|
||||
if (newQuery == "") {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
|
||||
presenter.restartPager(newQuery)
|
||||
fun setSources(sources: List<IFlexible<*>>) {
|
||||
adapter?.updateDataSet(sources)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request is received.
|
||||
*
|
||||
* @param page the current page.
|
||||
* @param mangas the list of manga of the page.
|
||||
* Called to set the last used catalogue at the top of the view.
|
||||
*/
|
||||
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
|
||||
val adapter = adapter ?: return
|
||||
hideProgressBar()
|
||||
if (page == 1) {
|
||||
adapter.clear()
|
||||
resetProgressItem()
|
||||
}
|
||||
adapter.onLoadMoreComplete(mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request fails.
|
||||
*
|
||||
* @param error the error received.
|
||||
*/
|
||||
fun onAddPageError(error: Throwable) {
|
||||
Timber.e(error)
|
||||
val adapter = adapter ?: return
|
||||
adapter.onLoadMoreComplete(null)
|
||||
hideProgressBar()
|
||||
|
||||
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
|
||||
|
||||
snack?.dismiss()
|
||||
snack = catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_retry) {
|
||||
// If not the first page, show bottom progress bar.
|
||||
if (adapter.mainItemCount > 0) {
|
||||
val item = progressItem ?: return@setAction
|
||||
adapter.addScrollableFooterWithDelay(item, 0, true)
|
||||
} else {
|
||||
showProgressBar()
|
||||
}
|
||||
presenter.requestNext()
|
||||
}
|
||||
fun setLastUsedSource(item: SourceItem?) {
|
||||
adapter?.removeAllScrollableHeaders()
|
||||
if (item != null) {
|
||||
adapter?.addScrollableHeader(item)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new progress item and reenables the scroll listener.
|
||||
*/
|
||||
private fun resetProgressItem() {
|
||||
progressItem = ProgressItem()
|
||||
adapter?.endlessTargetCount = 0
|
||||
adapter?.setEndlessScrollListener(this, progressItem!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the adapter when scrolled near the bottom.
|
||||
*/
|
||||
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
|
||||
if (presenter.hasNextPage()) {
|
||||
presenter.requestNext()
|
||||
} else {
|
||||
adapter?.onLoadMoreComplete(null)
|
||||
adapter?.endlessTargetCount = 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun noMoreLoad(newItemsSize: Int) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the manga initialized
|
||||
*/
|
||||
fun onMangaInitialized(manga: Manga) {
|
||||
getHolder(manga)?.setImage(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the current display mode.
|
||||
*/
|
||||
fun swapDisplayMode() {
|
||||
val view = view ?: return
|
||||
val adapter = adapter ?: return
|
||||
|
||||
presenter.swapDisplayMode()
|
||||
val isListMode = presenter.isListMode
|
||||
activity?.invalidateOptionsMenu()
|
||||
setupRecycler(view)
|
||||
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
|
||||
// Initialize mangas if going to grid view or if over wifi when going to list view
|
||||
val mangas = (0 until adapter.itemCount).mapNotNull {
|
||||
(adapter.getItem(it) as? CatalogueItem)?.manga
|
||||
}
|
||||
presenter.initializeMangas(mangas)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a preference for the number of manga per row based on the current orientation.
|
||||
*
|
||||
* @return the preference.
|
||||
*/
|
||||
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
preferences.portraitColumns()
|
||||
else
|
||||
preferences.landscapeColumns()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view holder for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(manga: Manga): CatalogueHolder? {
|
||||
val adapter = adapter ?: return null
|
||||
|
||||
adapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
|
||||
if (item != null && item.manga.id!! == manga.id!!) {
|
||||
return holder as CatalogueHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress bar.
|
||||
*/
|
||||
private fun showProgressBar() {
|
||||
progress?.visible()
|
||||
snack?.dismiss()
|
||||
snack = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides active progress bars.
|
||||
*/
|
||||
private fun hideProgressBar() {
|
||||
progress?.gone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
* @return true if the item should be selected, false otherwise.
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
|
||||
router.pushController(MangaController(item.manga, true).withFadeTransaction())
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is long clicked.
|
||||
*
|
||||
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
|
||||
* in, the list consists of the default category plus the user's categories. The default category is preselected on
|
||||
* new manga, and on already favorited manga the manga's categories are preselected.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
|
||||
if (manga.favorite) {
|
||||
MaterialDialog.Builder(activity)
|
||||
.items(activity.getString(R.string.remove_from_library))
|
||||
.itemsCallback { _, _, which, _ ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
|
||||
val categories = presenter.getCategories()
|
||||
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
|
||||
if (defaultCategory != null) {
|
||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
||||
} else if (categories.size <= 1) { // default or the one from the user
|
||||
presenter.moveMangaToCategory(manga, categories.firstOrNull())
|
||||
} else {
|
||||
val ids = presenter.getMangaCategoryIds(manga)
|
||||
val preselected = ids.mapNotNull { id ->
|
||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||
}.toTypedArray()
|
||||
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update manga to use selected categories.
|
||||
*
|
||||
* @param mangas The list of manga to move to categories.
|
||||
* @param categories The list of categories where manga will be placed.
|
||||
*/
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||
val manga = mangas.firstOrNull() ?: return
|
||||
presenter.updateMangaCategories(manga, categories)
|
||||
}
|
||||
|
||||
protected companion object {
|
||||
const val SOURCE_ID_KEY = "sourceId"
|
||||
}
|
||||
|
||||
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
|
||||
}
|
|
@ -1,376 +1,104 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.filter.*
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Presenter of [CatalogueController].
|
||||
* Presenter of [CatalogueController]
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param sourceManager manages the different sources.
|
||||
* @param preferences application preferences.
|
||||
*/
|
||||
open class CataloguePresenter(
|
||||
sourceId: Long,
|
||||
sourceManager: SourceManager = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val prefs: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
class CataloguePresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<CatalogueController>() {
|
||||
|
||||
/**
|
||||
* Selected source.
|
||||
* Enabled sources.
|
||||
*/
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
var sources = getEnabledSources()
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
* Subscription for retrieving enabled sources.
|
||||
*/
|
||||
var query = ""
|
||||
private set
|
||||
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
var sourceFilters = FilterList()
|
||||
set(value) {
|
||||
field = value
|
||||
filterItems = value.toItems()
|
||||
}
|
||||
|
||||
var filterItems: List<IFlexible<*>> = emptyList()
|
||||
|
||||
/**
|
||||
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
|
||||
*/
|
||||
var appliedFilters = FilterList()
|
||||
|
||||
/**
|
||||
* Pager containing a list of manga results.
|
||||
*/
|
||||
private lateinit var pager: Pager
|
||||
|
||||
/**
|
||||
* Subject that initializes a list of manga.
|
||||
*/
|
||||
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
|
||||
|
||||
/**
|
||||
* Whether the view is in list mode or not.
|
||||
*/
|
||||
var isListMode: Boolean = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subscription for the pager.
|
||||
*/
|
||||
private var pagerSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for one request from the pager.
|
||||
*/
|
||||
private var pageSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to initialize manga details.
|
||||
*/
|
||||
private var initializerSubscription: Subscription? = null
|
||||
private var sourceSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
sourceFilters = source.getFilterList()
|
||||
|
||||
if (savedState != null) {
|
||||
query = savedState.getString(CataloguePresenter::query.name, "")
|
||||
}
|
||||
|
||||
add(prefs.catalogueAsList().asObservable()
|
||||
.subscribe { setDisplayMode(it) })
|
||||
|
||||
restartPager()
|
||||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
state.putString(CataloguePresenter::query.name, query)
|
||||
super.onSave(state)
|
||||
// Load enabled and last used sources
|
||||
loadSources()
|
||||
loadLastUsedSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the pager for the active source with the provided query and filters.
|
||||
*
|
||||
* @param query the query.
|
||||
* @param filters the current state of the filters (for search mode).
|
||||
* Unsubscribe and create a new subscription to fetch enabled sources.
|
||||
*/
|
||||
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
|
||||
this.query = query
|
||||
this.appliedFilters = filters
|
||||
fun loadSources() {
|
||||
sourceSubscription?.unsubscribe()
|
||||
|
||||
subscribeToMangaInitializer()
|
||||
|
||||
// Create a new pager.
|
||||
pager = createPager(query, filters)
|
||||
|
||||
val sourceId = source.id
|
||||
|
||||
val catalogueAsList = prefs.catalogueAsList()
|
||||
|
||||
// Prepare the pager.
|
||||
pagerSubscription?.let { remove(it) }
|
||||
pagerSubscription = pager.results()
|
||||
.observeOn(Schedulers.io())
|
||||
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
|
||||
.doOnNext { initializeMangas(it.second) }
|
||||
.map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeReplay({ view, (page, mangas) ->
|
||||
view.onAddPage(page, mangas)
|
||||
}, { _, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
|
||||
// Request first page.
|
||||
requestNext()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the next page for the active pager.
|
||||
*/
|
||||
fun requestNext() {
|
||||
if (!hasNextPage()) return
|
||||
|
||||
pageSubscription?.let { remove(it) }
|
||||
pageSubscription = Observable.defer { pager.requestNext() }
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Nothing to do when onNext is emitted.
|
||||
}, CatalogueController::onAddPageError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the last fetched page has a next page.
|
||||
*/
|
||||
fun hasNextPage(): Boolean {
|
||||
return pager.hasNextPage
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display mode.
|
||||
*
|
||||
* @param asList whether the current mode is in list or not.
|
||||
*/
|
||||
private fun setDisplayMode(asList: Boolean) {
|
||||
isListMode = asList
|
||||
subscribeToMangaInitializer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||
*/
|
||||
private fun subscribeToMangaInitializer() {
|
||||
initializerSubscription?.let { remove(it) }
|
||||
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.thumbnail_url == null && !it.initialized }
|
||||
.concatMap { getMangaDetailsObservable(it) }
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ manga ->
|
||||
@Suppress("DEPRECATION")
|
||||
view?.onMangaInitialized(manga)
|
||||
}, { error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
.apply { add(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||
* if the manga is not yet in the database.
|
||||
*
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
newManga.copyFrom(sManga)
|
||||
val result = db.insertManga(newManga).executeAsBlocking()
|
||||
newManga.id = result.insertedId()
|
||||
localManga = newManga
|
||||
}
|
||||
return localManga
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a list of manga.
|
||||
*
|
||||
* @param mangas the list of manga to initialize.
|
||||
*/
|
||||
fun initializeMangas(mangas: List<Manga>) {
|
||||
mangaDetailSubject.onNext(mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of manga that initializes the given manga.
|
||||
*
|
||||
* @param manga the manga to initialize.
|
||||
* @return an observable of the manga to initialize
|
||||
*/
|
||||
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.flatMap { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
Observable.just(manga)
|
||||
}
|
||||
.onErrorResumeNext { Observable.just(manga) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes a manga from the library.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
fun changeMangaFavorite(manga: Manga) {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active display mode.
|
||||
*/
|
||||
fun swapDisplayMode() {
|
||||
prefs.catalogueAsList().set(!isListMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter states for the current source.
|
||||
*
|
||||
* @param filters a list of active filters.
|
||||
*/
|
||||
fun setSourceFilter(filters: FilterList) {
|
||||
restartPager(filters = filters)
|
||||
}
|
||||
|
||||
open fun createPager(query: String, filters: FilterList): Pager {
|
||||
return CataloguePager(source, query, filters)
|
||||
}
|
||||
|
||||
private fun FilterList.toItems(): List<IFlexible<*>> {
|
||||
return mapNotNull {
|
||||
when (it) {
|
||||
is Filter.Header -> HeaderItem(it)
|
||||
is Filter.Separator -> SeparatorItem(it)
|
||||
is Filter.CheckBox -> CheckboxItem(it)
|
||||
is Filter.TriState -> TriStateItem(it)
|
||||
is Filter.Text -> TextItem(it)
|
||||
is Filter.Select<*> -> SelectItem(it)
|
||||
is Filter.Group<*> -> {
|
||||
val group = GroupItem(it)
|
||||
val subItems = it.state.mapNotNull {
|
||||
when (it) {
|
||||
is Filter.CheckBox -> CheckboxSectionItem(it)
|
||||
is Filter.TriState -> TriStateSectionItem(it)
|
||||
is Filter.Text -> TextSectionItem(it)
|
||||
is Filter.Select<*> -> SelectSectionItem(it)
|
||||
else -> null
|
||||
} as? ISectionable<*, *>
|
||||
}
|
||||
subItems.forEach { it.header = group }
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
val group = SortGroup(it)
|
||||
val subItems = it.values.map {
|
||||
SortItem(it, group)
|
||||
}
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
|
||||
// Catalogues without a lang defined will be placed at the end
|
||||
when {
|
||||
d1 == "" && d2 != "" -> 1
|
||||
d2 == "" && d1 != "" -> -1
|
||||
else -> d1.compareTo(d2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default, and user categories.
|
||||
*
|
||||
* @return List of categories, default plus user categories
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||
*
|
||||
* @param manga the manga to get categories from.
|
||||
* @return Array of category ids the manga is in, if none returns default id
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
return categories.mapNotNull { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to the category.
|
||||
*
|
||||
* @param category the selected category.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||
moveMangaToCategories(manga, listOfNotNull(category))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update manga to use selected categories.
|
||||
*
|
||||
* @param manga needed to change
|
||||
* @param selectedCategories selected categories
|
||||
*/
|
||||
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
|
||||
if (!selectedCategories.isEmpty()) {
|
||||
if (!manga.favorite)
|
||||
changeMangaFavorite(manga)
|
||||
|
||||
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
|
||||
} else {
|
||||
changeMangaFavorite(manga)
|
||||
val byLang = sources.groupByTo(map, { it.lang })
|
||||
val sourceItems = byLang.flatMap {
|
||||
val langItem = LangItem(it.key)
|
||||
it.value.map { source -> SourceItem(source, langItem) }
|
||||
}
|
||||
|
||||
sourceSubscription = Observable.just(sourceItems)
|
||||
.subscribeLatestCache(CatalogueController::setSources)
|
||||
}
|
||||
|
||||
private fun loadLastUsedSource() {
|
||||
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
|
||||
|
||||
// Emit the first item immediately but delay subsequent emissions by 500ms.
|
||||
Observable.merge(
|
||||
sharedObs.take(1),
|
||||
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
|
||||
.distinctUntilChanged()
|
||||
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
|
||||
.subscribeLatestCache(CatalogueController::setLastUsedSource)
|
||||
}
|
||||
|
||||
fun updateSources() {
|
||||
sources = getEnabledSources()
|
||||
loadSources()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of enabled sources ordered by language and name.
|
||||
*
|
||||
* @return list containing enabled sources.
|
||||
*/
|
||||
private fun getEnabledSources(): List<CatalogueSource> {
|
||||
val languages = preferences.enabledLanguages().getOrDefault()
|
||||
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" } +
|
||||
sourceManager.get(LocalSource.ID) as LocalSource
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
@ -1,3 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
class NoResultsException : Exception()
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
|
@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.util.visible
|
|||
import io.github.mthli.slice.Slice
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.*
|
||||
|
||||
class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) {
|
||||
class SourceHolder(view: View, adapter: CatalogueAdapter) : FlexibleViewHolder(view, adapter) {
|
||||
|
||||
private val slice = Slice(itemView.card).apply {
|
||||
setColor(adapter.cardBackground)
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
|
@ -26,7 +26,7 @@ data class SourceItem(val source: CatalogueSource, val header: LangItem? = null)
|
|||
* Creates a new view holder for this item.
|
||||
*/
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder {
|
||||
return SourceHolder(view, adapter as CatalogueMainAdapter)
|
||||
return SourceHolder(view, adapter as CatalogueAdapter)
|
||||
}
|
||||
|
||||
/**
|
|
@ -0,0 +1,520 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
import android.support.v7.widget.*
|
||||
import android.view.*
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
|
||||
import kotlinx.android.synthetic.main.catalogue_controller.*
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subscriptions.Subscriptions
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Controller to manage the catalogues available in the app.
|
||||
*/
|
||||
open class BrowseCatalogueController(bundle: Bundle) :
|
||||
NucleusController<BrowseCataloguePresenter>(bundle),
|
||||
SecondaryDrawerController,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
ChangeMangaCategoriesDialog.Listener {
|
||||
|
||||
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
})
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Adapter containing the list of manga from the catalogue.
|
||||
*/
|
||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
|
||||
/**
|
||||
* Snackbar containing an error message when a request fails.
|
||||
*/
|
||||
private var snack: Snackbar? = null
|
||||
|
||||
/**
|
||||
* Navigation view containing filter items.
|
||||
*/
|
||||
private var navView: CatalogueNavigationView? = null
|
||||
|
||||
/**
|
||||
* Recycler view with the list of results.
|
||||
*/
|
||||
private var recycler: RecyclerView? = null
|
||||
|
||||
/**
|
||||
* Drawer listener to allow swipe only for closing the drawer.
|
||||
*/
|
||||
private var drawerListener: DrawerLayout.DrawerListener? = null
|
||||
|
||||
/**
|
||||
* Subscription for the search view.
|
||||
*/
|
||||
private var searchViewSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for the number of manga per row.
|
||||
*/
|
||||
private var numColumnsSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Endless loading item.
|
||||
*/
|
||||
private var progressItem: ProgressItem? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return presenter.source.name
|
||||
}
|
||||
|
||||
override fun createPresenter(): BrowseCataloguePresenter {
|
||||
return BrowseCataloguePresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.catalogue_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Initialize adapter, scroll listener and recycler views
|
||||
adapter = FlexibleAdapter(null, this)
|
||||
setupRecycler(view)
|
||||
|
||||
navView?.setFilters(presenter.filterItems)
|
||||
|
||||
progress?.visible()
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
numColumnsSubscription = null
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = null
|
||||
adapter = null
|
||||
snack = null
|
||||
recycler = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
|
||||
// Inflate and prepare drawer
|
||||
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView
|
||||
this.navView = navView
|
||||
drawerListener = DrawerSwipeCloseListener(drawer, navView).also {
|
||||
drawer.addDrawerListener(it)
|
||||
}
|
||||
navView.setFilters(presenter.filterItems)
|
||||
|
||||
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END)
|
||||
|
||||
navView.onSearchClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters)
|
||||
}
|
||||
|
||||
navView.onResetClicked = {
|
||||
presenter.appliedFilters = FilterList()
|
||||
val newFilters = presenter.source.getFilterList()
|
||||
presenter.sourceFilters = newFilters
|
||||
navView.setFilters(presenter.filterItems)
|
||||
}
|
||||
return navView
|
||||
}
|
||||
|
||||
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
|
||||
drawerListener?.let { drawer.removeDrawerListener(it) }
|
||||
drawerListener = null
|
||||
navView = null
|
||||
}
|
||||
|
||||
private fun setupRecycler(view: View) {
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
|
||||
var oldPosition = RecyclerView.NO_POSITION
|
||||
val oldRecycler = catalogue_view?.getChildAt(1)
|
||||
if (oldRecycler is RecyclerView) {
|
||||
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
oldRecycler.adapter = null
|
||||
|
||||
catalogue_view?.removeView(oldRecycler)
|
||||
}
|
||||
|
||||
val recycler = if (presenter.isListMode) {
|
||||
RecyclerView(view.context).apply {
|
||||
id = R.id.recycler
|
||||
layoutManager = LinearLayoutManager(context)
|
||||
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
}
|
||||
} else {
|
||||
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply {
|
||||
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable()
|
||||
.doOnNext { spanCount = it }
|
||||
.skip(1)
|
||||
// Set again the adapter to recalculate the covers height
|
||||
.subscribe { adapter = this@BrowseCatalogueController.adapter }
|
||||
|
||||
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return when (adapter?.getItemViewType(position)) {
|
||||
R.layout.catalogue_grid_item, null -> 1
|
||||
else -> spanCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.adapter = adapter
|
||||
|
||||
catalogue_view.addView(recycler, 1)
|
||||
|
||||
if (oldPosition != RecyclerView.NO_POSITION) {
|
||||
recycler.layoutManager.scrollToPosition(oldPosition)
|
||||
}
|
||||
this.recycler = recycler
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.catalogue_list, menu)
|
||||
|
||||
// Initialize search menu
|
||||
menu.findItem(R.id.action_search).apply {
|
||||
val searchView = actionView as SearchView
|
||||
|
||||
val query = presenter.query
|
||||
if (!query.isBlank()) {
|
||||
expandActionView()
|
||||
searchView.setQuery(query, true)
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
val searchEventsObservable = searchView.queryTextChangeEvents()
|
||||
.skip(1)
|
||||
.share()
|
||||
val writingObservable = searchEventsObservable
|
||||
.filter { !it.isSubmitted }
|
||||
.debounce(1250, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
|
||||
val submitObservable = searchEventsObservable
|
||||
.filter { it.isSubmitted }
|
||||
|
||||
searchViewSubscription?.unsubscribe()
|
||||
searchViewSubscription = Observable.merge(writingObservable, submitObservable)
|
||||
.map { it.queryText().toString() }
|
||||
.distinctUntilChanged()
|
||||
.subscribeUntilDestroy { searchWithQuery(it) }
|
||||
|
||||
untilDestroySubscriptions.add(
|
||||
Subscriptions.create { if (isActionViewExpanded) collapseActionView() })
|
||||
}
|
||||
|
||||
// Setup filters button
|
||||
menu.findItem(R.id.action_set_filter).apply {
|
||||
icon.mutate()
|
||||
if (presenter.sourceFilters.isEmpty()) {
|
||||
isEnabled = false
|
||||
icon.alpha = 128
|
||||
} else {
|
||||
isEnabled = true
|
||||
icon.alpha = 255
|
||||
}
|
||||
}
|
||||
|
||||
// Show next display mode
|
||||
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 -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the request with a new query.
|
||||
*
|
||||
* @param newQuery the new query.
|
||||
*/
|
||||
private fun searchWithQuery(newQuery: String) {
|
||||
// If text didn't change, do nothing
|
||||
if (presenter.query == newQuery)
|
||||
return
|
||||
|
||||
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
|
||||
if (newQuery == "") {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
showProgressBar()
|
||||
adapter?.clear()
|
||||
|
||||
presenter.restartPager(newQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request is received.
|
||||
*
|
||||
* @param page the current page.
|
||||
* @param mangas the list of manga of the page.
|
||||
*/
|
||||
fun onAddPage(page: Int, mangas: List<CatalogueItem>) {
|
||||
val adapter = adapter ?: return
|
||||
hideProgressBar()
|
||||
if (page == 1) {
|
||||
adapter.clear()
|
||||
resetProgressItem()
|
||||
}
|
||||
adapter.onLoadMoreComplete(mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the network request fails.
|
||||
*
|
||||
* @param error the error received.
|
||||
*/
|
||||
fun onAddPageError(error: Throwable) {
|
||||
Timber.e(error)
|
||||
val adapter = adapter ?: return
|
||||
adapter.onLoadMoreComplete(null)
|
||||
hideProgressBar()
|
||||
|
||||
val message = if (error is NoResultsException) "No results found" else (error.message ?: "")
|
||||
|
||||
snack?.dismiss()
|
||||
snack = catalogue_view?.snack(message, Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_retry) {
|
||||
// If not the first page, show bottom progress bar.
|
||||
if (adapter.mainItemCount > 0) {
|
||||
val item = progressItem ?: return@setAction
|
||||
adapter.addScrollableFooterWithDelay(item, 0, true)
|
||||
} else {
|
||||
showProgressBar()
|
||||
}
|
||||
presenter.requestNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new progress item and reenables the scroll listener.
|
||||
*/
|
||||
private fun resetProgressItem() {
|
||||
progressItem = ProgressItem()
|
||||
adapter?.endlessTargetCount = 0
|
||||
adapter?.setEndlessScrollListener(this, progressItem!!)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the adapter when scrolled near the bottom.
|
||||
*/
|
||||
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
|
||||
if (presenter.hasNextPage()) {
|
||||
presenter.requestNext()
|
||||
} else {
|
||||
adapter?.onLoadMoreComplete(null)
|
||||
adapter?.endlessTargetCount = 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun noMoreLoad(newItemsSize: Int) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when a manga is initialized.
|
||||
*
|
||||
* @param manga the manga initialized
|
||||
*/
|
||||
fun onMangaInitialized(manga: Manga) {
|
||||
getHolder(manga)?.setImage(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps the current display mode.
|
||||
*/
|
||||
fun swapDisplayMode() {
|
||||
val view = view ?: return
|
||||
val adapter = adapter ?: return
|
||||
|
||||
presenter.swapDisplayMode()
|
||||
val isListMode = presenter.isListMode
|
||||
activity?.invalidateOptionsMenu()
|
||||
setupRecycler(view)
|
||||
if (!isListMode || !view.context.connectivityManager.isActiveNetworkMetered) {
|
||||
// Initialize mangas if going to grid view or if over wifi when going to list view
|
||||
val mangas = (0 until adapter.itemCount).mapNotNull {
|
||||
(adapter.getItem(it) as? CatalogueItem)?.manga
|
||||
}
|
||||
presenter.initializeMangas(mangas)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a preference for the number of manga per row based on the current orientation.
|
||||
*
|
||||
* @return the preference.
|
||||
*/
|
||||
fun getColumnsPreferenceForCurrentOrientation(): Preference<Int> {
|
||||
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
preferences.portraitColumns()
|
||||
else
|
||||
preferences.landscapeColumns()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view holder for the given manga.
|
||||
*
|
||||
* @param manga the manga to find.
|
||||
* @return the holder of the manga or null if it's not bound.
|
||||
*/
|
||||
private fun getHolder(manga: Manga): CatalogueHolder? {
|
||||
val adapter = adapter ?: return null
|
||||
|
||||
adapter.allBoundViewHolders.forEach { holder ->
|
||||
val item = adapter.getItem(holder.adapterPosition) as? CatalogueItem
|
||||
if (item != null && item.manga.id!! == manga.id!!) {
|
||||
return holder as CatalogueHolder
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress bar.
|
||||
*/
|
||||
private fun showProgressBar() {
|
||||
progress?.visible()
|
||||
snack?.dismiss()
|
||||
snack = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides active progress bars.
|
||||
*/
|
||||
private fun hideProgressBar() {
|
||||
progress?.gone()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is clicked.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
* @return true if the item should be selected, false otherwise.
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
val item = adapter?.getItem(position) as? CatalogueItem ?: return false
|
||||
router.pushController(MangaController(item.manga, true).withFadeTransaction())
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a manga is long clicked.
|
||||
*
|
||||
* Adds the manga to the default category if none is set it shows a list of categories for the user to put the manga
|
||||
* in, the list consists of the default category plus the user's categories. The default category is preselected on
|
||||
* new manga, and on already favorited manga the manga's categories are preselected.
|
||||
*
|
||||
* @param position the position of the element clicked.
|
||||
*/
|
||||
override fun onItemLongClick(position: Int) {
|
||||
val activity = activity ?: return
|
||||
val manga = (adapter?.getItem(position) as? CatalogueItem?)?.manga ?: return
|
||||
if (manga.favorite) {
|
||||
MaterialDialog.Builder(activity)
|
||||
.items(activity.getString(R.string.remove_from_library))
|
||||
.itemsCallback { _, _, which, _ ->
|
||||
when (which) {
|
||||
0 -> {
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
}.show()
|
||||
} else {
|
||||
presenter.changeMangaFavorite(manga)
|
||||
adapter?.notifyItemChanged(position)
|
||||
|
||||
val categories = presenter.getCategories()
|
||||
val defaultCategory = categories.find { it.id == preferences.defaultCategory() }
|
||||
if (defaultCategory != null) {
|
||||
presenter.moveMangaToCategory(manga, defaultCategory)
|
||||
} else if (categories.size <= 1) { // default or the one from the user
|
||||
presenter.moveMangaToCategory(manga, categories.firstOrNull())
|
||||
} else {
|
||||
val ids = presenter.getMangaCategoryIds(manga)
|
||||
val preselected = ids.mapNotNull { id ->
|
||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
||||
}.toTypedArray()
|
||||
|
||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
||||
.showDialog(router)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update manga to use selected categories.
|
||||
*
|
||||
* @param mangas The list of manga to move to categories.
|
||||
* @param categories The list of categories where manga will be placed.
|
||||
*/
|
||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
||||
val manga = mangas.firstOrNull() ?: return
|
||||
presenter.updateMangaCategories(manga, categories)
|
||||
}
|
||||
|
||||
protected companion object {
|
||||
const val SOURCE_ID_KEY = "sourceId"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,376 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.davidea.flexibleadapter.items.ISectionable
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.filter.*
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* Presenter of [BrowseCatalogueController].
|
||||
*/
|
||||
open class BrowseCataloguePresenter(
|
||||
sourceId: Long,
|
||||
sourceManager: SourceManager = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val prefs: PreferencesHelper = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<BrowseCatalogueController>() {
|
||||
|
||||
/**
|
||||
* Selected source.
|
||||
*/
|
||||
val source = sourceManager.get(sourceId) as CatalogueSource
|
||||
|
||||
/**
|
||||
* Query from the view.
|
||||
*/
|
||||
var query = ""
|
||||
private set
|
||||
|
||||
/**
|
||||
* Modifiable list of filters.
|
||||
*/
|
||||
var sourceFilters = FilterList()
|
||||
set(value) {
|
||||
field = value
|
||||
filterItems = value.toItems()
|
||||
}
|
||||
|
||||
var filterItems: List<IFlexible<*>> = emptyList()
|
||||
|
||||
/**
|
||||
* List of filters used by the [Pager]. If empty alongside [query], the popular query is used.
|
||||
*/
|
||||
var appliedFilters = FilterList()
|
||||
|
||||
/**
|
||||
* Pager containing a list of manga results.
|
||||
*/
|
||||
private lateinit var pager: Pager
|
||||
|
||||
/**
|
||||
* Subject that initializes a list of manga.
|
||||
*/
|
||||
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
|
||||
|
||||
/**
|
||||
* Whether the view is in list mode or not.
|
||||
*/
|
||||
var isListMode: Boolean = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subscription for the pager.
|
||||
*/
|
||||
private var pagerSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription for one request from the pager.
|
||||
*/
|
||||
private var pageSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to initialize manga details.
|
||||
*/
|
||||
private var initializerSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
sourceFilters = source.getFilterList()
|
||||
|
||||
if (savedState != null) {
|
||||
query = savedState.getString(::query.name, "")
|
||||
}
|
||||
|
||||
add(prefs.catalogueAsList().asObservable()
|
||||
.subscribe { setDisplayMode(it) })
|
||||
|
||||
restartPager()
|
||||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
state.putString(::query.name, query)
|
||||
super.onSave(state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the pager for the active source with the provided query and filters.
|
||||
*
|
||||
* @param query the query.
|
||||
* @param filters the current state of the filters (for search mode).
|
||||
*/
|
||||
fun restartPager(query: String = this.query, filters: FilterList = this.appliedFilters) {
|
||||
this.query = query
|
||||
this.appliedFilters = filters
|
||||
|
||||
subscribeToMangaInitializer()
|
||||
|
||||
// Create a new pager.
|
||||
pager = createPager(query, filters)
|
||||
|
||||
val sourceId = source.id
|
||||
|
||||
val catalogueAsList = prefs.catalogueAsList()
|
||||
|
||||
// Prepare the pager.
|
||||
pagerSubscription?.let { remove(it) }
|
||||
pagerSubscription = pager.results()
|
||||
.observeOn(Schedulers.io())
|
||||
.map { it.first to it.second.map { networkToLocalManga(it, sourceId) } }
|
||||
.doOnNext { initializeMangas(it.second) }
|
||||
.map { it.first to it.second.map { CatalogueItem(it, catalogueAsList) } }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeReplay({ view, (page, mangas) ->
|
||||
view.onAddPage(page, mangas)
|
||||
}, { _, error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
|
||||
// Request first page.
|
||||
requestNext()
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the next page for the active pager.
|
||||
*/
|
||||
fun requestNext() {
|
||||
if (!hasNextPage()) return
|
||||
|
||||
pageSubscription?.let { remove(it) }
|
||||
pageSubscription = Observable.defer { pager.requestNext() }
|
||||
.subscribeFirst({ _, _ ->
|
||||
// Nothing to do when onNext is emitted.
|
||||
}, BrowseCatalogueController::onAddPageError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the last fetched page has a next page.
|
||||
*/
|
||||
fun hasNextPage(): Boolean {
|
||||
return pager.hasNextPage
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display mode.
|
||||
*
|
||||
* @param asList whether the current mode is in list or not.
|
||||
*/
|
||||
private fun setDisplayMode(asList: Boolean) {
|
||||
isListMode = asList
|
||||
subscribeToMangaInitializer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the initializer of manga details and updates the view if needed.
|
||||
*/
|
||||
private fun subscribeToMangaInitializer() {
|
||||
initializerSubscription?.let { remove(it) }
|
||||
initializerSubscription = mangaDetailSubject.observeOn(Schedulers.io())
|
||||
.flatMap { Observable.from(it) }
|
||||
.filter { it.thumbnail_url == null && !it.initialized }
|
||||
.concatMap { getMangaDetailsObservable(it) }
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ manga ->
|
||||
@Suppress("DEPRECATION")
|
||||
view?.onMangaInitialized(manga)
|
||||
}, { error ->
|
||||
Timber.e(error)
|
||||
})
|
||||
.apply { add(this) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||
* if the manga is not yet in the database.
|
||||
*
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
newManga.copyFrom(sManga)
|
||||
val result = db.insertManga(newManga).executeAsBlocking()
|
||||
newManga.id = result.insertedId()
|
||||
localManga = newManga
|
||||
}
|
||||
return localManga
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a list of manga.
|
||||
*
|
||||
* @param mangas the list of manga to initialize.
|
||||
*/
|
||||
fun initializeMangas(mangas: List<Manga>) {
|
||||
mangaDetailSubject.onNext(mangas)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable of manga that initializes the given manga.
|
||||
*
|
||||
* @param manga the manga to initialize.
|
||||
* @return an observable of the manga to initialize
|
||||
*/
|
||||
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.flatMap { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
Observable.just(manga)
|
||||
}
|
||||
.onErrorResumeNext { Observable.just(manga) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or removes a manga from the library.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
fun changeMangaFavorite(manga: Manga) {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active display mode.
|
||||
*/
|
||||
fun swapDisplayMode() {
|
||||
prefs.catalogueAsList().set(!isListMode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the filter states for the current source.
|
||||
*
|
||||
* @param filters a list of active filters.
|
||||
*/
|
||||
fun setSourceFilter(filters: FilterList) {
|
||||
restartPager(filters = filters)
|
||||
}
|
||||
|
||||
open fun createPager(query: String, filters: FilterList): Pager {
|
||||
return CataloguePager(source, query, filters)
|
||||
}
|
||||
|
||||
private fun FilterList.toItems(): List<IFlexible<*>> {
|
||||
return mapNotNull {
|
||||
when (it) {
|
||||
is Filter.Header -> HeaderItem(it)
|
||||
is Filter.Separator -> SeparatorItem(it)
|
||||
is Filter.CheckBox -> CheckboxItem(it)
|
||||
is Filter.TriState -> TriStateItem(it)
|
||||
is Filter.Text -> TextItem(it)
|
||||
is Filter.Select<*> -> SelectItem(it)
|
||||
is Filter.Group<*> -> {
|
||||
val group = GroupItem(it)
|
||||
val subItems = it.state.mapNotNull {
|
||||
when (it) {
|
||||
is Filter.CheckBox -> CheckboxSectionItem(it)
|
||||
is Filter.TriState -> TriStateSectionItem(it)
|
||||
is Filter.Text -> TextSectionItem(it)
|
||||
is Filter.Select<*> -> SelectSectionItem(it)
|
||||
else -> null
|
||||
} as? ISectionable<*, *>
|
||||
}
|
||||
subItems.forEach { it.header = group }
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
is Filter.Sort -> {
|
||||
val group = SortGroup(it)
|
||||
val subItems = it.values.map {
|
||||
SortItem(it, group)
|
||||
}
|
||||
group.subItems = subItems
|
||||
group
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default, and user categories.
|
||||
*
|
||||
* @return List of categories, default plus user categories
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||
*
|
||||
* @param manga the manga to get categories from.
|
||||
* @return Array of category ids the manga is in, if none returns default id
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int?> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
return categories.mapNotNull { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to categories.
|
||||
*
|
||||
* @param categories the selected categories.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to the category.
|
||||
*
|
||||
* @param category the selected category.
|
||||
* @param manga the manga to move.
|
||||
*/
|
||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||
moveMangaToCategories(manga, listOfNotNull(category))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update manga to use selected categories.
|
||||
*
|
||||
* @param manga needed to change
|
||||
* @param selectedCategories selected categories
|
||||
*/
|
||||
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) {
|
||||
if (!selectedCategories.isEmpty()) {
|
||||
if (!manga.favorite)
|
||||
changeMangaFavorite(manga)
|
||||
|
||||
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 })
|
||||
} else {
|
||||
changeMangaFavorite(manga)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.view.View
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.view.View
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
@ -0,0 +1,3 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
class NoResultsException : Exception()
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue
|
||||
package eu.kanade.tachiyomi.ui.catalogue.browse
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
|
@ -12,7 +12,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
|
|||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
|
@ -67,7 +67,7 @@ class CatalogueSearchPresenter(
|
|||
super.onCreate(savedState)
|
||||
|
||||
// Perform a search with previous or initial state
|
||||
search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty())
|
||||
search(savedState?.getString(BrowseCataloguePresenter::query.name) ?: initialQuery.orEmpty())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -77,7 +77,7 @@ class CatalogueSearchPresenter(
|
|||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
state.putString(CataloguePresenter::query.name, query)
|
||||
state.putString(BrowseCataloguePresenter::query.name, query)
|
||||
super.onSave(state)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package eu.kanade.tachiyomi.ui.latest_updates
|
||||
package eu.kanade.tachiyomi.ui.catalogue.latest
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
|
@ -6,19 +6,19 @@ import android.view.Menu
|
|||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseCatalogueController].
|
||||
*/
|
||||
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
|
||||
class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle) {
|
||||
|
||||
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
})
|
||||
|
||||
override fun createPresenter(): CataloguePresenter {
|
||||
override fun createPresenter(): BrowseCataloguePresenter {
|
||||
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package eu.kanade.tachiyomi.ui.latest_updates
|
||||
package eu.kanade.tachiyomi.ui.catalogue.latest
|
||||
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
|
@ -0,0 +1,16 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.latest
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.browse.Pager
|
||||
|
||||
/**
|
||||
* Presenter of [LatestUpdatesController]. Inherit BrowseCataloguePresenter.
|
||||
*/
|
||||
class LatestUpdatesPresenter(sourceId: Long) : BrowseCataloguePresenter(sourceId) {
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
return LatestUpdatesPager(source)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,231 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.view.*
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||
import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
/**
|
||||
* This controller shows and manages the different catalogues enabled by the user.
|
||||
* This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter]
|
||||
* [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
|
||||
* [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click.
|
||||
* [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click
|
||||
*/
|
||||
class CatalogueMainController : NucleusController<CatalogueMainPresenter>(),
|
||||
SourceLoginDialog.Listener,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
CatalogueMainAdapter.OnBrowseClickListener,
|
||||
CatalogueMainAdapter.OnLatestClickListener {
|
||||
|
||||
/**
|
||||
* Application preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
|
||||
/**
|
||||
* Adapter containing sources.
|
||||
*/
|
||||
private var adapter : CatalogueMainAdapter? = null
|
||||
|
||||
/**
|
||||
* Called when controller is initialized.
|
||||
*/
|
||||
init {
|
||||
// Enable the option menu
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title of controller.
|
||||
*
|
||||
* @return title.
|
||||
*/
|
||||
override fun getTitle(): String? {
|
||||
return applicationContext?.getString(R.string.label_catalogues)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [CatalogueMainPresenter] used in controller.
|
||||
*
|
||||
* @return instance of [CatalogueMainPresenter]
|
||||
*/
|
||||
override fun createPresenter(): CatalogueMainPresenter {
|
||||
return CatalogueMainPresenter()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the view with [R.layout.catalogue_main_controller].
|
||||
*
|
||||
* @param inflater used to load the layout xml.
|
||||
* @param container containing parent views.
|
||||
* @return inflated view.
|
||||
*/
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.catalogue_main_controller, container, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the view is created
|
||||
*
|
||||
* @param view view of controller
|
||||
*/
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = CatalogueMainAdapter(this)
|
||||
|
||||
// Create recycler and set adapter.
|
||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
recycler.adapter = adapter
|
||||
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
|
||||
presenter.updateSources()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when login dialog is closed, refreshes the adapter.
|
||||
*
|
||||
* @param source clicked item containing source information.
|
||||
*/
|
||||
override fun loginDialogClosed(source: LoginSource) {
|
||||
if (source.isLogged()) {
|
||||
adapter?.clear()
|
||||
presenter.loadSources()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when item is clicked
|
||||
*/
|
||||
override fun onItemClick(position: Int): Boolean {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||
val source = item.source
|
||||
if (source is LoginSource && !source.isLogged()) {
|
||||
val dialog = SourceLoginDialog(source)
|
||||
dialog.targetController = this
|
||||
dialog.showDialog(router)
|
||||
} else {
|
||||
// Open the catalogue view.
|
||||
openCatalogue(source, CatalogueController(source))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when browse is clicked in [CatalogueMainAdapter]
|
||||
*/
|
||||
override fun onBrowseClick(position: Int) {
|
||||
onItemClick(position)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when latest is clicked in [CatalogueMainAdapter]
|
||||
*/
|
||||
override fun onLatestClick(position: Int) {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||
openCatalogue(item.source, LatestUpdatesController(item.source))
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a catalogue with the given controller.
|
||||
*/
|
||||
private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) {
|
||||
preferences.lastUsedCatalogueSource().set(source.id)
|
||||
router.pushController(controller.withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds items to the options menu.
|
||||
*
|
||||
* @param menu menu containing options.
|
||||
* @param inflater used to load the menu xml.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
// Inflate menu
|
||||
inflater.inflate(R.menu.catalogue_main, menu)
|
||||
|
||||
// Initialize search option.
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
// Change hint to show global search.
|
||||
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
|
||||
|
||||
// Create query listener which opens the global search view.
|
||||
searchView.queryTextChangeEvents()
|
||||
.filter { it.isSubmitted }
|
||||
.subscribeUntilDestroy {
|
||||
val query = it.queryText().toString()
|
||||
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an option menu item has been selected by the user.
|
||||
*
|
||||
* @param item The selected item.
|
||||
* @return True if this event has been consumed, false if it has not.
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
// Initialize option to open catalogue settings.
|
||||
R.id.action_settings -> {
|
||||
router.pushController((RouterTransaction.with(SettingsSourcesController()))
|
||||
.popChangeHandler(SettingsSourcesFadeChangeHandler())
|
||||
.pushChangeHandler(FadeChangeHandler()))
|
||||
}
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update adapter containing sources.
|
||||
*/
|
||||
fun setSources(sources: List<IFlexible<*>>) {
|
||||
adapter?.updateDataSet(sources)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to set the last used catalogue at the top of the view.
|
||||
*/
|
||||
fun setLastUsedSource(item: SourceItem?) {
|
||||
adapter?.removeAllScrollableHeaders()
|
||||
if (item != null) {
|
||||
adapter?.addScrollableHeader(item)
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.catalogue.main
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Presenter of [CatalogueMainController]
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param sourceManager manages the different sources.
|
||||
* @param preferences application preferences.
|
||||
*/
|
||||
class CatalogueMainPresenter(
|
||||
val sourceManager: SourceManager = Injekt.get(),
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
) : BasePresenter<CatalogueMainController>() {
|
||||
|
||||
/**
|
||||
* Enabled sources.
|
||||
*/
|
||||
var sources = getEnabledSources()
|
||||
|
||||
/**
|
||||
* Subscription for retrieving enabled sources.
|
||||
*/
|
||||
private var sourceSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Load enabled and last used sources
|
||||
loadSources()
|
||||
loadLastUsedSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe and create a new subscription to fetch enabled sources.
|
||||
*/
|
||||
fun loadSources() {
|
||||
sourceSubscription?.unsubscribe()
|
||||
|
||||
val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
|
||||
// Catalogues without a lang defined will be placed at the end
|
||||
when {
|
||||
d1 == "" && d2 != "" -> 1
|
||||
d2 == "" && d1 != "" -> -1
|
||||
else -> d1.compareTo(d2)
|
||||
}
|
||||
}
|
||||
val byLang = sources.groupByTo(map, { it.lang })
|
||||
val sourceItems = byLang.flatMap {
|
||||
val langItem = LangItem(it.key)
|
||||
it.value.map { source -> SourceItem(source, langItem) }
|
||||
}
|
||||
|
||||
sourceSubscription = Observable.just(sourceItems)
|
||||
.subscribeLatestCache(CatalogueMainController::setSources)
|
||||
}
|
||||
|
||||
private fun loadLastUsedSource() {
|
||||
val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
|
||||
|
||||
// Emit the first item immediately but delay subsequent emissions by 500ms.
|
||||
Observable.merge(
|
||||
sharedObs.take(1),
|
||||
sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
|
||||
.distinctUntilChanged()
|
||||
.map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
|
||||
.subscribeLatestCache(CatalogueMainController::setLastUsedSource)
|
||||
}
|
||||
|
||||
fun updateSources() {
|
||||
sources = getEnabledSources()
|
||||
loadSources()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of enabled sources ordered by language and name.
|
||||
*
|
||||
* @return list containing enabled sources.
|
||||
*/
|
||||
private fun getEnabledSources(): List<CatalogueSource> {
|
||||
val languages = preferences.enabledLanguages().getOrDefault()
|
||||
val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
|
||||
|
||||
return sourceManager.getCatalogueSources()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||
.sortedBy { "(${it.lang}) ${it.name}" } +
|
||||
sourceManager.get(LocalSource.ID) as LocalSource
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.latest_updates
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.Pager
|
||||
|
||||
/**
|
||||
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
|
||||
*/
|
||||
class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
return LatestUpdatesPager(source)
|
||||
}
|
||||
|
||||
}
|
|
@ -9,13 +9,12 @@ import android.support.v4.widget.DrawerLayout
|
|||
import android.support.v7.graphics.drawable.DrawerArrowDrawable
|
||||
import android.view.ViewGroup
|
||||
import com.bluelinelabs.conductor.*
|
||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||
import eu.kanade.tachiyomi.Migrations
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.base.controller.*
|
||||
import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
|
@ -80,7 +79,7 @@ class MainActivity : BaseActivity() {
|
|||
R.id.nav_drawer_library -> setRoot(LibraryController(), id)
|
||||
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
|
||||
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
|
||||
R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id)
|
||||
R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
|
||||
R.id.nav_drawer_downloads -> {
|
||||
router.pushController(DownloadController().withFadeTransaction())
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"
|
||||
android:id="@id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/catalogue_view"
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController">
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<eu.kanade.tachiyomi.ui.catalogue.CatalogueNavigationView
|
||||
<eu.kanade.tachiyomi.ui.catalogue.browse.CatalogueNavigationView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/nav_view2"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"
|
||||
tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"
|
||||
android:id="@id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
|
Loading…
Reference in a new issue