From 297fed6aef50b334d2de2867b84b4ca26fa5c387 Mon Sep 17 00:00:00 2001 From: inorichi Date: Sun, 3 Dec 2017 12:58:38 +0100 Subject: [PATCH] Repackage catalogue to match the UI --- ...ogueMainAdapter.kt => CatalogueAdapter.kt} | 10 +- .../ui/catalogue/CatalogueController.kt | 751 ++++++------------ .../ui/catalogue/CataloguePresenter.kt | 406 ++-------- .../ui/catalogue/{main => }/LangHolder.kt | 40 +- .../ui/catalogue/{main => }/LangItem.kt | 2 +- .../ui/catalogue/NoResultsException.kt | 3 - .../{main => }/SourceDividerItemDecoration.kt | 92 +-- .../ui/catalogue/{main => }/SourceHolder.kt | 4 +- .../ui/catalogue/{main => }/SourceItem.kt | 4 +- .../browse/BrowseCatalogueController.kt | 520 ++++++++++++ .../browse/BrowseCataloguePresenter.kt | 376 +++++++++ .../{ => browse}/CatalogueGridHolder.kt | 2 +- .../catalogue/{ => browse}/CatalogueHolder.kt | 2 +- .../catalogue/{ => browse}/CatalogueItem.kt | 2 +- .../{ => browse}/CatalogueListHolder.kt | 2 +- .../{ => browse}/CatalogueNavigationView.kt | 78 +- .../catalogue/{ => browse}/CataloguePager.kt | 2 +- .../ui/catalogue/browse/NoResultsException.kt | 3 + .../ui/catalogue/{ => browse}/Pager.kt | 2 +- .../ui/catalogue/{ => browse}/ProgressItem.kt | 2 +- .../global_search/CatalogueSearchPresenter.kt | 6 +- .../latest}/LatestUpdatesController.kt | 78 +- .../latest}/LatestUpdatesPager.kt | 4 +- .../latest/LatestUpdatesPresenter.kt | 16 + .../catalogue/main/CatalogueMainController.kt | 231 ------ .../catalogue/main/CatalogueMainPresenter.kt | 104 --- .../latest_updates/LatestUpdatesPresenter.kt | 16 - .../kanade/tachiyomi/ui/main/MainActivity.kt | 5 +- .../res/layout-land/manga_info_controller.xml | 2 +- .../main/res/layout/catalogue_controller.xml | 2 +- app/src/main/res/layout/catalogue_drawer.xml | 2 +- .../main/res/layout/manga_info_controller.xml | 2 +- 32 files changed, 1385 insertions(+), 1386 deletions(-) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{main/CatalogueMainAdapter.kt => CatalogueAdapter.kt} (76%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{main => }/LangHolder.kt (90%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{main => }/LangItem.kt (95%) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{main => }/SourceDividerItemDecoration.kt (94%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{main => }/SourceHolder.kt (95%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{main => }/SourceItem.kt (90%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/CatalogueGridHolder.kt (97%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/CatalogueHolder.kt (95%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/CatalogueItem.kt (97%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/CatalogueListHolder.kt (97%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/CatalogueNavigationView.kt (93%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/CataloguePager.kt (95%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/Pager.kt (94%) rename app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/{ => browse}/ProgressItem.kt (96%) rename app/src/main/java/eu/kanade/tachiyomi/ui/{latest_updates => catalogue/latest}/LatestUpdatesController.kt (69%) rename app/src/main/java/eu/kanade/tachiyomi/ui/{latest_updates => catalogue/latest}/LatestUpdatesPager.kt (85%) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueAdapter.kt similarity index 76% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueAdapter.kt index d2e15169c..1b1f09d9a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueAdapter.kt @@ -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>(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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt index cf6c3d6f7..59d0293b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt @@ -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(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>? = 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) { - 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 { - 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, categories: List) { - 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(), + 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>) { + 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() +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index 62a6977ba..096c5b19e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -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() { /** - * 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> = 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>() - - /** - * 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) { - 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 { - 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> { - 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> { 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 { - 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 { - 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) { - 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) { - 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 { + 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 + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt similarity index 90% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt index 02dcea146..ea1fe6f0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangHolder.kt @@ -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() + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangItem.kt similarity index 95% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangItem.kt index 815ad7495..dfa6a91d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/LangItem.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt deleted file mode 100644 index 3ac0dbac8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/NoResultsException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package eu.kanade.tachiyomi.ui.catalogue - -class NoResultsException : Exception() \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt index bb90f5307..4caa72053 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt similarity index 95% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt index ddc8914b0..e361b7b9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceItem.kt similarity index 90% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceItem.kt index 5031b81d9..57c1fdf8a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceItem.kt @@ -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) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt new file mode 100644 index 000000000..e5edc0297 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCatalogueController.kt @@ -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(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>? = 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) { + 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 { + 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, categories: List) { + val manga = mangas.firstOrNull() ?: return + presenter.updateMangaCategories(manga, categories) + } + + protected companion object { + const val SOURCE_ID_KEY = "sourceId" + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt new file mode 100644 index 000000000..6bc440eae --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/BrowseCataloguePresenter.kt @@ -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() { + + /** + * 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> = 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>() + + /** + * 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) { + 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 { + 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> { + 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 { + 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 { + 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) { + 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) { + if (!selectedCategories.isEmpty()) { + if (!manga.favorite) + changeMangaFavorite(manga) + + moveMangaToCategories(manga, selectedCategories.filter { it.id != 0 }) + } else { + changeMangaFavorite(manga) + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueGridHolder.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueGridHolder.kt index 3fdba1e2e..6d3d2e746 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueGridHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueHolder.kt similarity index 95% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueHolder.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueHolder.kt index 014b7904a..174bb8075 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueItem.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueItem.kt index 0a3209810..71696565b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueItem.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueListHolder.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueListHolder.kt index a12ec77d2..928a304d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueListHolder.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt similarity index 93% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt index c55727129..fd2d2685a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CatalogueNavigationView.kt @@ -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> = FlexibleAdapter>(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>) { - 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> = FlexibleAdapter>(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>) { + adapter.updateDataSet(items) + } + } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CataloguePager.kt similarity index 95% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CataloguePager.kt index b95798a7d..a7b563074 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/CataloguePager.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt new file mode 100644 index 000000000..723782f5e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/NoResultsException.kt @@ -0,0 +1,3 @@ +package eu.kanade.tachiyomi.ui.catalogue.browse + +class NoResultsException : Exception() \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/Pager.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/Pager.kt index a0f3d55e2..1383fdfcc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/Pager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/Pager.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/ProgressItem.kt similarity index 96% rename from app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/ProgressItem.kt index 279017a74..0eeeb51d3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/ProgressItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/browse/ProgressItem.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt index ebd5327d6..fac3a5b6c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt @@ -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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesController.kt similarity index 69% rename from app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesController.kt index 072980607..221a2142e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesController.kt @@ -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) { + + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPager.kt similarity index 85% rename from app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt rename to app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPager.kt index 7cdcdff66..2e646638b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPager.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt new file mode 100644 index 000000000..a1be55797 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/latest/LatestUpdatesPresenter.kt @@ -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) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt deleted file mode 100644 index f6aff8ab7..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt +++ /dev/null @@ -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(), - 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>) { - 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() -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt deleted file mode 100644 index a0745a26a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt +++ /dev/null @@ -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() { - - /** - * 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> { 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 { - 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 - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt deleted file mode 100644 index 2e0ea07fe..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt +++ /dev/null @@ -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) - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 77b4cc016..53c557948 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -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()) } diff --git a/app/src/main/res/layout-land/manga_info_controller.xml b/app/src/main/res/layout-land/manga_info_controller.xml index fa49622b5..ad371aaa8 100644 --- a/app/src/main/res/layout-land/manga_info_controller.xml +++ b/app/src/main/res/layout-land/manga_info_controller.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController" + tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController" android:id="@id/swipe_refresh" android:layout_width="match_parent" android:layout_height="match_parent"> diff --git a/app/src/main/res/layout/catalogue_controller.xml b/app/src/main/res/layout/catalogue_controller.xml index 17ba20e10..929ea2275 100644 --- a/app/src/main/res/layout/catalogue_controller.xml +++ b/app/src/main/res/layout/catalogue_controller.xml @@ -11,7 +11,7 @@ android:fitsSystemWindows="true" android:orientation="vertical" android:id="@+id/catalogue_view" - tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"> + tools:context="eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController"> -