Repackage catalogue to match the UI

pull/1111/head
inorichi 7 years ago
parent d690d6e0e3
commit 297fed6aef

@ -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.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
@ -8,9 +8,9 @@ import eu.kanade.tachiyomi.util.getResourceColor
/** /**
* Adapter that holds the catalogue cards. * 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) { FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) 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. * 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 { interface OnBrowseClickListener {
fun onBrowseClick(position: Int) fun onBrowseClick(position: Int)
@ -39,7 +39,7 @@ class CatalogueMainAdapter(val controller: CatalogueMainController) :
/** /**
* Listener which should be called when user clicks latest. * 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 { interface OnLatestClickListener {
fun onLatestClick(position: Int) fun onLatestClick(position: Int)

@ -1,520 +1,231 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.content.res.Configuration import android.support.v7.widget.LinearLayoutManager
import android.os.Bundle import android.support.v7.widget.SearchView
import android.support.design.widget.Snackbar import android.view.*
import android.support.v4.widget.DrawerLayout import com.bluelinelabs.conductor.ControllerChangeHandler
import android.support.v7.widget.* import com.bluelinelabs.conductor.ControllerChangeType
import android.view.* import com.bluelinelabs.conductor.RouterTransaction
import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
import eu.kanade.tachiyomi.ui.manga.MangaController import kotlinx.android.synthetic.main.catalogue_main_controller.*
import eu.kanade.tachiyomi.util.* import uy.kohesive.injekt.Injekt
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import uy.kohesive.injekt.api.get
import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener
import kotlinx.android.synthetic.main.catalogue_controller.* /**
import kotlinx.android.synthetic.main.main_activity.* * This controller shows and manages the different catalogues enabled by the user.
import rx.Observable * This controller should only handle UI actions, IO actions should be done by [CataloguePresenter]
import rx.Subscription * [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
import rx.android.schedulers.AndroidSchedulers * [CatalogueAdapter.OnBrowseClickListener] call function data on browse item click.
import rx.subscriptions.Subscriptions * [CatalogueAdapter.OnLatestClickListener] call function data on latest item click
import timber.log.Timber */
import uy.kohesive.injekt.injectLazy class CatalogueController : NucleusController<CataloguePresenter>(),
import java.util.concurrent.TimeUnit SourceLoginDialog.Listener,
FlexibleAdapter.OnItemClickListener,
/** CatalogueAdapter.OnBrowseClickListener,
* Controller to manage the catalogues available in the app. CatalogueAdapter.OnLatestClickListener {
*/
open class CatalogueController(bundle: Bundle) : /**
NucleusController<CataloguePresenter>(bundle), * Application preferences.
SecondaryDrawerController, */
FlexibleAdapter.OnItemClickListener, private val preferences: PreferencesHelper = Injekt.get()
FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.EndlessScrollListener, /**
ChangeMangaCategoriesDialog.Listener { * Adapter containing sources.
*/
constructor(source: CatalogueSource) : this(Bundle().apply { private var adapter : CatalogueAdapter? = null
putLong(SOURCE_ID_KEY, source.id)
}) /**
* Called when controller is initialized.
/** */
* Preferences helper. init {
*/ // Enable the option menu
private val preferences: PreferencesHelper by injectLazy() setHasOptionsMenu(true)
}
/**
* Adapter containing the list of manga from the catalogue. /**
*/ * Set the title of controller.
private var adapter: FlexibleAdapter<IFlexible<*>>? = null *
* @return title.
/** */
* Snackbar containing an error message when a request fails. override fun getTitle(): String? {
*/ return applicationContext?.getString(R.string.label_catalogues)
private var snack: Snackbar? = null }
/** /**
* Navigation view containing filter items. * Create the [CataloguePresenter] used in controller.
*/ *
private var navView: CatalogueNavigationView? = null * @return instance of [CataloguePresenter]
*/
/** override fun createPresenter(): CataloguePresenter {
* Recycler view with the list of results. return CataloguePresenter()
*/ }
private var recycler: RecyclerView? = null
/**
/** * Initiate the view with [R.layout.catalogue_main_controller].
* Drawer listener to allow swipe only for closing the drawer. *
*/ * @param inflater used to load the layout xml.
private var drawerListener: DrawerLayout.DrawerListener? = null * @param container containing parent views.
* @return inflated view.
/** */
* Subscription for the search view. override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
*/ return inflater.inflate(R.layout.catalogue_main_controller, container, false)
private var searchViewSubscription: Subscription? = null }
/** /**
* Subscription for the number of manga per row. * Called when the view is created
*/ *
private var numColumnsSubscription: Subscription? = null * @param view view of controller
*/
/** override fun onViewCreated(view: View) {
* Endless loading item. super.onViewCreated(view)
*/
private var progressItem: ProgressItem? = null adapter = CatalogueAdapter(this)
init { // Create recycler and set adapter.
setHasOptionsMenu(true) recycler.layoutManager = LinearLayoutManager(view.context)
} recycler.adapter = adapter
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
override fun getTitle(): String? { }
return presenter.source.name
} override fun onDestroyView(view: View) {
adapter = null
override fun createPresenter(): CataloguePresenter { super.onDestroyView(view)
return CataloguePresenter(args.getLong(SOURCE_ID_KEY)) }
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { super.onChangeStarted(handler, type)
return inflater.inflate(R.layout.catalogue_controller, container, false) if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
} presenter.updateSources()
}
override fun onViewCreated(view: View) { }
super.onViewCreated(view)
/**
// Initialize adapter, scroll listener and recycler views * Called when login dialog is closed, refreshes the adapter.
adapter = FlexibleAdapter(null, this) *
setupRecycler(view) * @param source clicked item containing source information.
*/
navView?.setFilters(presenter.filterItems) override fun loginDialogClosed(source: LoginSource) {
if (source.isLogged()) {
progress?.visible() adapter?.clear()
} presenter.loadSources()
}
override fun onDestroyView(view: View) { }
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null /**
searchViewSubscription?.unsubscribe() * Called when item is clicked
searchViewSubscription = null */
adapter = null override fun onItemClick(position: Int): Boolean {
snack = null val item = adapter?.getItem(position) as? SourceItem ?: return false
recycler = null val source = item.source
super.onDestroyView(view) if (source is LoginSource && !source.isLogged()) {
} val dialog = SourceLoginDialog(source)
dialog.targetController = this
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? { dialog.showDialog(router)
// Inflate and prepare drawer } else {
val navView = drawer.inflate(R.layout.catalogue_drawer) as CatalogueNavigationView // Open the catalogue view.
this.navView = navView openCatalogue(source, BrowseCatalogueController(source))
drawerListener = DrawerSwipeCloseListener(drawer, navView).also { }
drawer.addDrawerListener(it) return false
} }
navView.setFilters(presenter.filterItems)
/**
drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, Gravity.END) * Called when browse is clicked in [CatalogueAdapter]
*/
navView.onSearchClicked = { override fun onBrowseClick(position: Int) {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() onItemClick(position)
showProgressBar() }
adapter?.clear()
presenter.setSourceFilter(if (allDefault) FilterList() else presenter.sourceFilters) /**
} * Called when latest is clicked in [CatalogueAdapter]
*/
navView.onResetClicked = { override fun onLatestClick(position: Int) {
presenter.appliedFilters = FilterList() val item = adapter?.getItem(position) as? SourceItem ?: return
val newFilters = presenter.source.getFilterList() openCatalogue(item.source, LatestUpdatesController(item.source))
presenter.sourceFilters = newFilters }
navView.setFilters(presenter.filterItems)
} /**
return navView * Opens a catalogue with the given controller.
} */
private fun openCatalogue(source: CatalogueSource, controller: BrowseCatalogueController) {
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { preferences.lastUsedCatalogueSource().set(source.id)
drawerListener?.let { drawer.removeDrawerListener(it) } router.pushController(controller.withFadeTransaction())
drawerListener = null }
navView = null
} /**
* Adds items to the options menu.
private fun setupRecycler(view: View) { *
numColumnsSubscription?.unsubscribe() * @param menu menu containing options.
* @param inflater used to load the menu xml.
var oldPosition = RecyclerView.NO_POSITION */
val oldRecycler = catalogue_view?.getChildAt(1) override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (oldRecycler is RecyclerView) { // Inflate menu
oldPosition = (oldRecycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() inflater.inflate(R.menu.catalogue_main, menu)
oldRecycler.adapter = null
// Initialize search option.
catalogue_view?.removeView(oldRecycler) val searchItem = menu.findItem(R.id.action_search)
} val searchView = searchItem.actionView as SearchView
val recycler = if (presenter.isListMode) { // Change hint to show global search.
RecyclerView(view.context).apply { searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
id = R.id.recycler
layoutManager = LinearLayoutManager(context) // Create query listener which opens the global search view.
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) searchView.queryTextChangeEvents()
} .filter { it.isSubmitted }
} else { .subscribeUntilDestroy {
(catalogue_view.inflate(R.layout.catalogue_recycler_autofit) as AutofitRecyclerView).apply { val query = it.queryText().toString()
numColumnsSubscription = getColumnsPreferenceForCurrentOrientation().asObservable() router.pushController(CatalogueSearchController(query).withFadeTransaction())
.doOnNext { spanCount = it } }
.skip(1) }
// Set again the adapter to recalculate the covers height
.subscribe { adapter = this@CatalogueController.adapter } /**
* Called when an option menu item has been selected by the user.
(layoutManager as GridLayoutManager).spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { *
override fun getSpanSize(position: Int): Int { * @param item The selected item.
return when (adapter?.getItemViewType(position)) { * @return True if this event has been consumed, false if it has not.
R.layout.catalogue_grid_item, null -> 1 */
else -> spanCount 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())
recycler.setHasFixedSize(true) .pushChangeHandler(FadeChangeHandler()))
recycler.adapter = adapter }
else -> return super.onOptionsItemSelected(item)
catalogue_view.addView(recycler, 1) }
return true
if (oldPosition != RecyclerView.NO_POSITION) { }
recycler.layoutManager.scrollToPosition(oldPosition)
} /**
this.recycler = recycler * Called to update adapter containing sources.
} */
fun setSources(sources: List<IFlexible<*>>) {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { adapter?.updateDataSet(sources)
inflater.inflate(R.menu.catalogue_list, menu) }
// Initialize search menu /**
menu.findItem(R.id.action_search).apply { * Called to set the last used catalogue at the top of the view.
val searchView = actionView as SearchView */
fun setLastUsedSource(item: SourceItem?) {
val query = presenter.query adapter?.removeAllScrollableHeaders()
if (!query.isBlank()) { if (item != null) {
expandActionView() adapter?.addScrollableHeader(item)
searchView.setQuery(query, true) }
searchView.clearFocus() }
}
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
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"
}
}

@ -1,376 +1,104 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue
import android.os.Bundle 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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager 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.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers 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.Injekt
import uy.kohesive.injekt.api.get 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( class CataloguePresenter(
sourceId: Long, val sourceManager: SourceManager = Injekt.get(),
sourceManager: SourceManager = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get()
private val db: DatabaseHelper = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get()
) : BasePresenter<CatalogueController>() { ) : 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 var sourceSubscription: Subscription? = null
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?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
sourceFilters = source.getFilterList() // Load enabled and last used sources
loadSources()
if (savedState != null) { loadLastUsedSource()
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)
}
/**
* 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.
}, 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. * Unsubscribe and create a new subscription to fetch enabled sources.
*/
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) { fun loadSources() {
restartPager(filters = filters) sourceSubscription?.unsubscribe()
}
open fun createPager(query: String, filters: FilterList): Pager {
return CataloguePager(source, query, filters)
}
private fun FilterList.toItems(): List<IFlexible<*>> { val map = TreeMap<String, MutableList<CatalogueSource>> { d1, d2 ->
return mapNotNull { // Catalogues without a lang defined will be placed at the end
when (it) { when {
is Filter.Header -> HeaderItem(it) d1 == "" && d2 != "" -> 1
is Filter.Separator -> SeparatorItem(it) d2 == "" && d1 != "" -> -1
is Filter.CheckBox -> CheckboxItem(it) else -> d1.compareTo(d2)
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 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)
* Get the default, and user categories. .subscribeLatestCache(CatalogueController::setSources)
*
* @return List of categories, default plus user categories
*/
fun getCategories(): List<Category> {
return db.getCategories().executeAsBlocking()
} }
/** private fun loadLastUsedSource() {
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id. val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
*
* @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()
}
/** // Emit the first item immediately but delay subsequent emissions by 500ms.
* Move the given manga to categories. Observable.merge(
* sharedObs.take(1),
* @param categories the selected categories. sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
* @param manga the manga to move. .distinctUntilChanged()
*/ .map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
private fun moveMangaToCategories(manga: Manga, categories: List<Category>) { .subscribeLatestCache(CatalogueController::setLastUsedSource)
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
} }
/** fun updateSources() {
* Move the given manga to the category. sources = getEnabledSources()
* loadSources()
* @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. * Returns a list of enabled sources ordered by language and name.
* *
* @param manga needed to change * @return list containing enabled sources.
* @param selectedCategories selected categories
*/ */
fun updateMangaCategories(manga: Manga, selectedCategories: List<Category>) { private fun getEnabledSources(): List<CatalogueSource> {
if (!selectedCategories.isEmpty()) { val languages = preferences.enabledLanguages().getOrDefault()
if (!manga.favorite) val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
changeMangaFavorite(manga)
moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 }) return sourceManager.getCatalogueSources()
} else { .filter { it.lang in languages }
changeMangaFavorite(manga) .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 package eu.kanade.tachiyomi.ui.catalogue
import android.view.View import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.* import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
import java.util.* import java.util.*
class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) { class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
fun bind(item: LangItem) { fun bind(item: LangItem) {
itemView.title.text = when { itemView.title.text = when {
item.code == "" -> itemView.context.getString(R.string.other_source) item.code == "" -> itemView.context.getString(R.string.other_source)
else -> { else -> {
val locale = Locale(item.code) val locale = Locale(item.code)
locale.getDisplayName(locale).capitalize() 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 android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter 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 package eu.kanade.tachiyomi.ui.catalogue
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private val divider: Drawable private val divider: Drawable
init { init {
val a = context.obtainStyledAttributes(ATTRS) val a = context.obtainStyledAttributes(ATTRS)
divider = a.getDrawable(0) divider = a.getDrawable(0)
a.recycle() a.recycle()
} }
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
val left = parent.paddingLeft + SourceHolder.margin val left = parent.paddingLeft + SourceHolder.margin
val right = parent.width - parent.paddingRight - SourceHolder.margin val right = parent.width - parent.paddingRight - SourceHolder.margin
val childCount = parent.childCount val childCount = parent.childCount
for (i in 0 until childCount - 1) { for (i in 0 until childCount - 1) {
val child = parent.getChildAt(i) val child = parent.getChildAt(i)
if (parent.getChildViewHolder(child) is SourceHolder && if (parent.getChildViewHolder(child) is SourceHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) { parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
val params = child.layoutParams as RecyclerView.LayoutParams val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight val bottom = top + divider.intrinsicHeight
divider.setBounds(left, top, right, bottom) divider.setBounds(left, top, right, bottom)
divider.draw(c) divider.draw(c)
} }
} }
} }
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
state: RecyclerView.State) { state: RecyclerView.State) {
outRect.set(0, 0, 0, divider.intrinsicHeight) outRect.set(0, 0, 0, divider.intrinsicHeight)
} }
companion object { companion object {
private val ATTRS = intArrayOf(android.R.attr.listDivider) 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.os.Build
import android.view.View import android.view.View
@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.util.visible
import io.github.mthli.slice.Slice import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.* 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 { private val slice = Slice(itemView.card).apply {
setColor(adapter.cardBackground) 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 android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter 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. * Creates a new view holder for this item.
*/ */
override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder { 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 android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy 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 android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter 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.Gravity
import android.view.View 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 android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy

@ -1,40 +1,40 @@
package eu.kanade.tachiyomi.ui.catalogue package eu.kanade.tachiyomi.ui.catalogue.browse
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.ViewGroup import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.widget.SimpleNavigationView import eu.kanade.tachiyomi.widget.SimpleNavigationView
import kotlinx.android.synthetic.main.catalogue_drawer_content.view.* import kotlinx.android.synthetic.main.catalogue_drawer_content.view.*
class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) class CatalogueNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
: SimpleNavigationView(context, attrs) { : SimpleNavigationView(context, attrs) {
val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null) val adapter: FlexibleAdapter<IFlexible<*>> = FlexibleAdapter<IFlexible<*>>(null)
.setDisplayHeadersAtStartUp(true) .setDisplayHeadersAtStartUp(true)
.setStickyHeaders(true) .setStickyHeaders(true)
var onSearchClicked = {} var onSearchClicked = {}
var onResetClicked = {} var onResetClicked = {}
init { init {
recycler.adapter = adapter recycler.adapter = adapter
recycler.setHasFixedSize(true) recycler.setHasFixedSize(true)
val view = inflate(R.layout.catalogue_drawer_content) val view = inflate(R.layout.catalogue_drawer_content)
((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler) ((view as ViewGroup).getChildAt(1) as ViewGroup).addView(recycler)
addView(view) addView(view)
search_btn.setOnClickListener { onSearchClicked() } search_btn.setOnClickListener { onSearchClicked() }
reset_btn.setOnClickListener { onResetClicked() } reset_btn.setOnClickListener { onResetClicked() }
} }
fun setFilters(items: List<IFlexible<*>>) { fun setFilters(items: List<IFlexible<*>>) {
adapter.updateDataSet(items) 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.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList 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 com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.source.model.MangasPage 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.view.View
import android.widget.ProgressBar 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.model.SManga
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter 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.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -67,7 +67,7 @@ class CatalogueSearchPresenter(
super.onCreate(savedState) super.onCreate(savedState)
// Perform a search with previous or initial state // 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() { override fun onDestroy() {
@ -77,7 +77,7 @@ class CatalogueSearchPresenter(
} }
override fun onSave(state: Bundle) { override fun onSave(state: Bundle) {
state.putString(CataloguePresenter::query.name, query) state.putString(BrowseCataloguePresenter::query.name, query)
super.onSave(state) super.onSave(state)
} }

@ -1,39 +1,39 @@
package eu.kanade.tachiyomi.ui.latest_updates package eu.kanade.tachiyomi.ui.catalogue.latest
import android.os.Bundle import android.os.Bundle
import android.support.v4.widget.DrawerLayout import android.support.v4.widget.DrawerLayout
import android.view.Menu import android.view.Menu
import android.view.ViewGroup import android.view.ViewGroup
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCataloguePresenter
/** /**
* Controller that shows the latest manga from the catalogue. Inherit [CatalogueController]. * Controller that shows the latest manga from the catalogue. Inherit [BrowseCatalogueController].
*/ */
class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) { class LatestUpdatesController(bundle: Bundle) : BrowseCatalogueController(bundle) {
constructor(source: CatalogueSource) : this(Bundle().apply { constructor(source: CatalogueSource) : this(Bundle().apply {
putLong(SOURCE_ID_KEY, source.id) putLong(SOURCE_ID_KEY, source.id)
}) })
override fun createPresenter(): CataloguePresenter { override fun createPresenter(): BrowseCataloguePresenter {
return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_search).isVisible = false menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_set_filter).isVisible = false menu.findItem(R.id.action_set_filter).isVisible = false
} }
override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? { override fun createSecondaryDrawer(drawer: DrawerLayout): ViewGroup? {
return null return null
} }
override fun cleanupSecondaryDrawer(drawer: DrawerLayout) { 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.CatalogueSource
import eu.kanade.tachiyomi.source.model.MangasPage 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.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers 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.support.v7.graphics.drawable.DrawerArrowDrawable
import android.view.ViewGroup import android.view.ViewGroup
import com.bluelinelabs.conductor.* import com.bluelinelabs.conductor.*
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.* 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.download.DownloadController
import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController 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_library -> setRoot(LibraryController(), id)
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id) R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), 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 -> { R.id.nav_drawer_downloads -> {
router.pushController(DownloadController().withFadeTransaction()) router.pushController(DownloadController().withFadeTransaction())
} }

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController" tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"
android:id="@id/swipe_refresh" android:id="@id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

@ -11,7 +11,7 @@
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:orientation="vertical" android:orientation="vertical"
android:id="@+id/catalogue_view" android:id="@+id/catalogue_view"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"> tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController">
<ProgressBar <ProgressBar
android:id="@+id/progress" android:id="@+id/progress"

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<eu.kanade.tachiyomi.ui.catalogue.CatalogueNavigationView <eu.kanade.tachiyomi.ui.catalogue.browse.CatalogueNavigationView
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/nav_view2" android:id="@+id/nav_view2"
android:layout_width="wrap_content" android:layout_width="wrap_content"

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController" tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"
android:id="@id/swipe_refresh" android:id="@id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">

Loading…
Cancel
Save