mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Repackage catalogue to match the UI
This commit is contained in:
		@@ -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.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 CatalogueController(bundle: Bundle) :
 | 
			
		||||
        NucleusController<CataloguePresenter>(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(): CataloguePresenter {
 | 
			
		||||
        return CataloguePresenter(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@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
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        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"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
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.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
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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
 | 
			
		||||
 */
 | 
			
		||||
class CatalogueController : NucleusController<CataloguePresenter>(),
 | 
			
		||||
        SourceLoginDialog.Listener,
 | 
			
		||||
        FlexibleAdapter.OnItemClickListener,
 | 
			
		||||
        CatalogueAdapter.OnBrowseClickListener,
 | 
			
		||||
        CatalogueAdapter.OnLatestClickListener {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Application preferences.
 | 
			
		||||
     */
 | 
			
		||||
    private val preferences: PreferencesHelper = Injekt.get()
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Adapter containing sources.
 | 
			
		||||
     */
 | 
			
		||||
    private var adapter : CatalogueAdapter? = 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 [CataloguePresenter] used in controller.
 | 
			
		||||
     *
 | 
			
		||||
     * @return instance of [CataloguePresenter]
 | 
			
		||||
     */
 | 
			
		||||
    override fun createPresenter(): CataloguePresenter {
 | 
			
		||||
        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_main_controller, container, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Called when the view is created
 | 
			
		||||
     *
 | 
			
		||||
     * @param view view of controller
 | 
			
		||||
     */
 | 
			
		||||
    override fun onViewCreated(view: View) {
 | 
			
		||||
        super.onViewCreated(view)
 | 
			
		||||
 | 
			
		||||
        adapter = CatalogueAdapter(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, BrowseCatalogueController(source))
 | 
			
		||||
        }
 | 
			
		||||
        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) {
 | 
			
		||||
        // 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,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,21 +1,21 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.main
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
 | 
			
		||||
 | 
			
		||||
    fun bind(item: LangItem) {
 | 
			
		||||
        itemView.title.text = when {
 | 
			
		||||
            item.code == "" -> itemView.context.getString(R.string.other_source)
 | 
			
		||||
            else -> {
 | 
			
		||||
                val locale = Locale(item.code)
 | 
			
		||||
                locale.getDisplayName(locale).capitalize()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
 | 
			
		||||
 | 
			
		||||
    fun bind(item: LangItem) {
 | 
			
		||||
        itemView.title.text = when {
 | 
			
		||||
            item.code == "" -> itemView.context.getString(R.string.other_source)
 | 
			
		||||
            else -> {
 | 
			
		||||
                val locale = Locale(item.code)
 | 
			
		||||
                locale.getDisplayName(locale).capitalize()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,47 +1,47 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.main
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Canvas
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
 | 
			
		||||
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
 | 
			
		||||
 | 
			
		||||
    private val divider: Drawable
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val a = context.obtainStyledAttributes(ATTRS)
 | 
			
		||||
        divider = a.getDrawable(0)
 | 
			
		||||
        a.recycle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
 | 
			
		||||
        val left = parent.paddingLeft + SourceHolder.margin
 | 
			
		||||
        val right = parent.width - parent.paddingRight - SourceHolder.margin
 | 
			
		||||
 | 
			
		||||
        val childCount = parent.childCount
 | 
			
		||||
        for (i in 0 until childCount - 1) {
 | 
			
		||||
            val child = parent.getChildAt(i)
 | 
			
		||||
            if (parent.getChildViewHolder(child) is SourceHolder &&
 | 
			
		||||
                    parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
 | 
			
		||||
                val params = child.layoutParams as RecyclerView.LayoutParams
 | 
			
		||||
                val top = child.bottom + params.bottomMargin
 | 
			
		||||
                val bottom = top + divider.intrinsicHeight
 | 
			
		||||
 | 
			
		||||
                divider.setBounds(left, top, right, bottom)
 | 
			
		||||
                divider.draw(c)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
 | 
			
		||||
                                state: RecyclerView.State) {
 | 
			
		||||
        outRect.set(0, 0, 0, divider.intrinsicHeight)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val ATTRS = intArrayOf(android.R.attr.listDivider)
 | 
			
		||||
    }
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.graphics.Canvas
 | 
			
		||||
import android.graphics.Rect
 | 
			
		||||
import android.graphics.drawable.Drawable
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
 | 
			
		||||
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
 | 
			
		||||
 | 
			
		||||
    private val divider: Drawable
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        val a = context.obtainStyledAttributes(ATTRS)
 | 
			
		||||
        divider = a.getDrawable(0)
 | 
			
		||||
        a.recycle()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
 | 
			
		||||
        val left = parent.paddingLeft + SourceHolder.margin
 | 
			
		||||
        val right = parent.width - parent.paddingRight - SourceHolder.margin
 | 
			
		||||
 | 
			
		||||
        val childCount = parent.childCount
 | 
			
		||||
        for (i in 0 until childCount - 1) {
 | 
			
		||||
            val child = parent.getChildAt(i)
 | 
			
		||||
            if (parent.getChildViewHolder(child) is SourceHolder &&
 | 
			
		||||
                    parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
 | 
			
		||||
                val params = child.layoutParams as RecyclerView.LayoutParams
 | 
			
		||||
                val top = child.bottom + params.bottomMargin
 | 
			
		||||
                val bottom = top + divider.intrinsicHeight
 | 
			
		||||
 | 
			
		||||
                divider.setBounds(left, top, right, bottom)
 | 
			
		||||
                divider.draw(c)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
 | 
			
		||||
                                state: RecyclerView.State) {
 | 
			
		||||
        outRect.set(0, 0, 0, divider.intrinsicHeight)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        private val ATTRS = intArrayOf(android.R.attr.listDivider)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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,40 +1,40 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
 | 
			
		||||
    : SimpleNavigationView(context, attrs) {
 | 
			
		||||
 | 
			
		||||
    val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
 | 
			
		||||
            .setDisplayHeadersAtStartUp(true)
 | 
			
		||||
            .setStickyHeaders(true)
 | 
			
		||||
 | 
			
		||||
    var onSearchClicked = {}
 | 
			
		||||
 | 
			
		||||
    var onResetClicked = {}
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
        val view = inflate(R.layout.catalogue_drawer_content)
 | 
			
		||||
        ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
 | 
			
		||||
        addView(view)
 | 
			
		||||
 | 
			
		||||
        search_btn.setOnClickListener { onSearchClicked() }
 | 
			
		||||
        reset_btn.setOnClickListener { onResetClicked() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setFilters(items: List<IFlexible<*>>) {
 | 
			
		||||
        adapter.updateDataSet(items)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.browse
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import eu.davidea.flexibleadapter.FlexibleAdapter
 | 
			
		||||
import eu.davidea.flexibleadapter.items.IFlexible
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.util.inflate
 | 
			
		||||
import eu.kanade.tachiyomi.widget.SimpleNavigationView
 | 
			
		||||
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
 | 
			
		||||
    : SimpleNavigationView(context, attrs) {
 | 
			
		||||
 | 
			
		||||
    val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
 | 
			
		||||
            .setDisplayHeadersAtStartUp(true)
 | 
			
		||||
            .setStickyHeaders(true)
 | 
			
		||||
 | 
			
		||||
    var onSearchClicked = {}
 | 
			
		||||
 | 
			
		||||
    var onResetClicked = {}
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        recycler.adapter = adapter
 | 
			
		||||
        recycler.setHasFixedSize(true)
 | 
			
		||||
        val view = inflate(R.layout.catalogue_drawer_content)
 | 
			
		||||
        ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
 | 
			
		||||
        addView(view)
 | 
			
		||||
 | 
			
		||||
        search_btn.setOnClickListener { onSearchClicked() }
 | 
			
		||||
        reset_btn.setOnClickListener { onResetClicked() }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setFilters(items: List<IFlexible<*>>) {
 | 
			
		||||
        adapter.updateDataSet(items)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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,39 +1,39 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.latest_updates
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.widget.DrawerLayout
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
 | 
			
		||||
 */
 | 
			
		||||
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
 | 
			
		||||
 | 
			
		||||
    constructor(source: CatalogueSource) : this(Bundle().apply {
 | 
			
		||||
        putLong(SOURCE_ID_KEY, source.id)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): CataloguePresenter {
 | 
			
		||||
        return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        super.onPrepareOptionsMenu(menu)
 | 
			
		||||
        menu.findItem(R.id.action_search).isVisible = false
 | 
			
		||||
        menu.findItem(R.id.action_set_filter).isVisible = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
package eu.kanade.tachiyomi.ui.catalogue.latest
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.support.v4.widget.DrawerLayout
 | 
			
		||||
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.browse.BrowseCatalogueController
 | 
			
		||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller that shows the latest manga from the catalogue. Inherit [BrowseCatalogueController].
 | 
			
		||||
 */
 | 
			
		||||
class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle) {
 | 
			
		||||
 | 
			
		||||
    constructor(source: CatalogueSource) : this(Bundle().apply {
 | 
			
		||||
        putLong(SOURCE_ID_KEY, source.id)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    override fun createPresenter(): BrowseCataloguePresenter {
 | 
			
		||||
        return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onPrepareOptionsMenu(menu: Menu) {
 | 
			
		||||
        super.onPrepareOptionsMenu(menu)
 | 
			
		||||
        menu.findItem(R.id.action_search).isVisible = false
 | 
			
		||||
        menu.findItem(R.id.action_set_filter).isVisible = false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
 | 
			
		||||
        return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun cleanupSecondaryDrawer(drawer: DrawerLayout) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -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())
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user