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