From 54c8b3ef29d5ddb05925e119c41c37d95f189fdc Mon Sep 17 00:00:00 2001 From: Bram van de Kerkhof Date: Sat, 23 Sep 2017 13:11:39 +0200 Subject: [PATCH] Global Search (#849) * Global Search * Cards are now independent of design by use of recycler. * Added local * Some attribute fixes + moved onclick to controller. * Lots of improvements to code * Reversed some stuff. Thanks API 16 * Code fixes * Performance improvements * Moved adapter creation to constructor * Small changes * Removed sources settings from settings menu. Added OnChangeListener in catalogue. Made setting icon visible if room. * bug fix * Code review part uno * Code review part uno-2 * Single recycler approach * Add last source used * Fix scroll state and some layout issues * Fix wrong item binding * Use data class for items * Calculate item position and count while binding * Fix background color with slices * Reuse slices. Fix card background. Flatten constraint layout * Fix global_search scroll issue * Store last state with global search * Minor changes * Remove catalogue toolbar spinner. Persist catalogue across process restarts * Save view state of recycler views. Set toolbar title with current query --- app/build.gradle | 3 +- .../ui/base/controller/BaseController.kt | 2 +- .../ui/base/controller/NucleusController.kt | 2 +- .../ui/catalogue/CatalogueController.kt | 90 ++----- .../ui/catalogue/CataloguePresenter.kt | 94 +------ .../global_search/CatalogueSearchAdapter.kt | 74 ++++++ .../CatalogueSearchCardAdapter.kt | 27 ++ .../CatalogueSearchCardHolder.kt | 43 ++++ .../global_search/CatalogueSearchCardItem.kt | 38 +++ .../CatalogueSearchController.kt | 171 +++++++++++++ .../global_search/CatalogueSearchHolder.kt | 100 ++++++++ .../global_search/CatalogueSearchItem.kt | 67 +++++ .../global_search/CatalogueSearchPresenter.kt | 215 ++++++++++++++++ .../ui/catalogue/main/CatalogueMainAdapter.kt | 48 ++++ .../catalogue/main/CatalogueMainController.kt | 238 ++++++++++++++++++ .../catalogue/main/CatalogueMainPresenter.kt | 97 +++++++ .../tachiyomi/ui/catalogue/main/LangHolder.kt | 21 ++ .../tachiyomi/ui/catalogue/main/LangItem.kt | 41 +++ .../main/SourceDividerItemDecoration.kt | 47 ++++ .../ui/catalogue/main/SourceHolder.kt | 107 ++++++++ .../tachiyomi/ui/catalogue/main/SourceItem.kt | 45 ++++ .../tachiyomi/ui/category/CategoryHolder.kt | 20 +- .../latest_updates/LatestUpdatesController.kt | 14 +- .../latest_updates/LatestUpdatesPresenter.kt | 12 +- .../kanade/tachiyomi/ui/main/MainActivity.kt | 8 +- .../ui/setting/SettingsMainController.kt | 6 - .../ui/setting/SettingsSourcesController.kt | 2 + .../tachiyomi/util/ContextExtensions.kt | 2 +- .../kanade/tachiyomi/util/ViewExtensions.kt | 21 ++ .../library_item_selector_amoled.xml | 4 +- .../library_item_selector_dark.xml | 4 +- .../library_item_selector_light.xml | 4 +- .../list_item_selector_amoled.xml | 2 +- .../drawable-v21/list_item_selector_dark.xml | 2 +- .../drawable-v21/list_item_selector_light.xml | 2 +- .../drawable-v21/list_item_selector_trans.xml | 6 + .../res/drawable/ic_search_black_112dp.xml | 9 + .../drawable/library_item_selector_amoled.xml | 4 +- .../drawable/library_item_selector_dark.xml | 4 +- .../drawable/library_item_selector_light.xml | 13 +- .../drawable/list_item_selector_amoled.xml | 2 +- .../res/drawable/list_item_selector_dark.xml | 2 +- .../res/drawable/list_item_selector_light.xml | 11 +- .../res/drawable/list_item_selector_trans.xml | 10 + app/src/main/res/drawable/text_button.xml | 15 ++ .../main/res/layout/catalogue_controller.xml | 12 +- .../catalogue_global_search_controller.xml | 14 ++ ...atalogue_global_search_controller_card.xml | 83 ++++++ ...gue_global_search_controller_card_item.xml | 55 ++++ .../main/res/layout/catalogue_grid_item.xml | 2 +- .../main/res/layout/catalogue_list_item.xml | 3 +- .../res/layout/catalogue_main_controller.xml | 14 ++ .../layout/catalogue_main_controller_card.xml | 18 ++ .../catalogue_main_controller_card_item.xml | 72 ++++++ .../res/layout/catalogue_recycler_autofit.xml | 2 +- app/src/main/res/layout/categories_item.xml | 3 +- .../main/res/layout/library_grid_recycler.xml | 2 +- app/src/main/res/menu/catalogue_main.xml | 16 ++ app/src/main/res/menu/catalogue_new_list.xml | 11 + app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/styles.xml | 49 ++-- 61 files changed, 1852 insertions(+), 262 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt create mode 100644 app/src/main/res/drawable-v21/list_item_selector_trans.xml create mode 100644 app/src/main/res/drawable/ic_search_black_112dp.xml create mode 100644 app/src/main/res/drawable/list_item_selector_trans.xml create mode 100644 app/src/main/res/drawable/text_button.xml create mode 100644 app/src/main/res/layout/catalogue_global_search_controller.xml create mode 100644 app/src/main/res/layout/catalogue_global_search_controller_card.xml create mode 100644 app/src/main/res/layout/catalogue_global_search_controller_card_item.xml create mode 100644 app/src/main/res/layout/catalogue_main_controller.xml create mode 100644 app/src/main/res/layout/catalogue_main_controller_card.xml create mode 100644 app/src/main/res/layout/catalogue_main_controller_card_item.xml create mode 100644 app/src/main/res/menu/catalogue_main.xml create mode 100644 app/src/main/res/menu/catalogue_new_list.xml diff --git a/app/build.gradle b/app/build.gradle index 7c5577ad75..9564168e1c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,6 +191,7 @@ dependencies { compile 'com.afollestad.material-dialogs:core:0.9.4.5' compile 'me.zhanghai.android.systemuihelper:library:1.0.0' compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4' + compile 'com.github.mthli:Slice:v1.2' // Conductor compile "com.bluelinelabs:conductor:2.1.4" @@ -275,4 +276,4 @@ afterEvaluate { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt index 9f55cd033b..07649e2fd4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt @@ -34,7 +34,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr return null } - private fun setTitle() { + fun setTitle() { var parentController = parentController while (parentController != null) { if (parentController is BaseController && parentController.getTitle() != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt index 63eba25ed2..3f252409c9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt @@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory import nucleus.presenter.Presenter @Suppress("LeakingThis") -abstract class NucleusController

>(val bundle: Bundle? = null) : RxController(), +abstract class NucleusController

>(val bundle: Bundle? = null) : RxController(bundle), PresenterFactory

{ private val delegate = NucleusConductorDelegate(this) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt index a98ee3e2eb..dc53757dde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt @@ -4,24 +4,20 @@ 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.app.AppCompatActivity import android.support.v7.widget.* import android.view.* -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Spinner import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler import com.f2prateek.rx.preferences.Preference import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents -import com.jakewharton.rxbinding.widget.itemSelections 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 @@ -43,7 +39,7 @@ import java.util.concurrent.TimeUnit /** * Controller to manage the catalogues available in the app. */ -open class CatalogueController(bundle: Bundle? = null) : +open class CatalogueController(bundle: Bundle) : NucleusController(bundle), SecondaryDrawerController, FlexibleAdapter.OnItemClickListener, @@ -51,6 +47,10 @@ open class CatalogueController(bundle: Bundle? = null) : FlexibleAdapter.EndlessScrollListener, ChangeMangaCategoriesDialog.Listener { + constructor(source: CatalogueSource) : this(Bundle().apply { + putLong(SOURCE_ID_KEY, source.id) + }) + /** * Preferences helper. */ @@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) : */ private var adapter: FlexibleAdapter>? = null - /** - * Spinner shown in the toolbar to change the selected source. - */ - private var spinner: Spinner? = null - /** * Snackbar containing an error message when a request fails. */ @@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) : */ private var recycler: RecyclerView? = null + /** + * Drawer listener to allow swipe only for closing the drawer. + */ private var drawerListener: DrawerLayout.DrawerListener? = null - /** - * Query of the search box. - */ - private val query: String - get() = presenter.query - - /** - * Selected index of the spinner (selected source). - */ - private var selectedIndex: Int = 0 - /** * 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 { @@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) : } override fun getTitle(): String? { - return "" + return presenter.source.toString() } override fun createPresenter(): CataloguePresenter { - return CataloguePresenter() + return CataloguePresenter(args.getLong(SOURCE_ID_KEY)) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { @@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) : adapter = FlexibleAdapter(null, this) setupRecycler(view) - // Create toolbar spinner - val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext - ?: activity - - val spinnerAdapter = ArrayAdapter(themedContext, - android.R.layout.simple_spinner_item, presenter.sources) - spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item) - - val onItemSelected: (Int) -> Unit = { position -> - val source = spinnerAdapter.getItem(position) - if (!presenter.isValidSource(source)) { - spinner?.setSelection(selectedIndex) - activity?.toast(R.string.source_requires_login) - } else if (source != presenter.source) { - selectedIndex = position - showProgressBar() - adapter?.clear() - presenter.setActiveSource(source) - navView?.setFilters(presenter.filterItems) - activity?.invalidateOptionsMenu() - } - } - - selectedIndex = presenter.sources.indexOf(presenter.source) - - spinner = Spinner(themedContext).apply { - adapter = spinnerAdapter - setSelection(selectedIndex) - itemSelections() - .skip(1) - .filter { it != AdapterView.INVALID_POSITION } - .subscribeUntilDestroy { onItemSelected(it) } - } - - activity?.toolbar?.addView(spinner) + navView?.setFilters(presenter.filterItems) view.progress?.visible() } override fun onDestroyView(view: View) { super.onDestroyView(view) - activity?.toolbar?.removeView(spinner) numColumnsSubscription?.unsubscribe() numColumnsSubscription = null searchViewSubscription?.unsubscribe() searchViewSubscription = null adapter = null - spinner = null snack = null recycler = null } @@ -265,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) : menu.findItem(R.id.action_search).apply { val searchView = actionView as SearchView + val query = presenter.query if (!query.isBlank()) { expandActionView() searchView.setQuery(query, true) @@ -328,7 +286,7 @@ open class CatalogueController(bundle: Bundle? = null) : */ private fun searchWithQuery(newQuery: String) { // If text didn't change, do nothing - if (query == newQuery) + if (presenter.query == newQuery) return // FIXME dirty fix to restore the toolbar buttons after closing search mode. @@ -447,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) : */ fun getColumnsPreferenceForCurrentOrientation(): Preference { return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT) - presenter.prefs.portraitColumns() + preferences.portraitColumns() else - presenter.prefs.landscapeColumns() + preferences.landscapeColumns() } /** @@ -558,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) : presenter.updateMangaCategories(manga, categories) } + protected companion object { + const val SOURCE_ID_KEY = "sourceId" + } + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index 69fd34db74..82e1b4d566 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -9,15 +9,11 @@ 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.Source 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.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.catalogue.filter.* import rx.Observable @@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get * Presenter of [CatalogueController]. */ open class CataloguePresenter( - val sourceManager: SourceManager = Injekt.get(), - val db: DatabaseHelper = Injekt.get(), - val prefs: PreferencesHelper = Injekt.get(), - val coverCache: CoverCache = Injekt.get() + 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() { /** - * Enabled sources. + * Selected source. */ - val sources by lazy { getEnabledSources() } - - /** - * Active source. - */ - lateinit var source: CatalogueSource - private set + val source = sourceManager.get(sourceId) as CatalogueSource /** * Query from the view. @@ -106,7 +97,6 @@ open class CataloguePresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - source = getLastUsedSource() sourceFilters = source.getFilterList() if (savedState != null) { @@ -149,9 +139,9 @@ open class CataloguePresenter( .doOnNext { initializeMangas(it.second) } .map { it.first to it.second.map(::CatalogueItem) } .observeOn(AndroidSchedulers.mainThread()) - .subscribeReplay({ view, pair -> - view.onAddPage(pair.first, pair.second) - }, { view, error -> + .subscribeReplay({ view, (page, mangas) -> + view.onAddPage(page, mangas) + }, { _, error -> Timber.e(error) }) @@ -167,7 +157,7 @@ open class CataloguePresenter( pageSubscription?.let { remove(it) } pageSubscription = Observable.defer { pager.requestNext() } - .subscribeFirst({ view, page -> + .subscribeFirst({ _, _ -> // Nothing to do when onNext is emitted. }, CatalogueController::onAddPageError) } @@ -179,19 +169,6 @@ open class CataloguePresenter( return pager.hasNextPage } - /** - * Sets the active source and restarts the pager. - * - * @param source the new active source. - */ - fun setActiveSource(source: CatalogueSource) { - prefs.lastUsedCatalogueSource().set(source.id) - this.source = source - sourceFilters = source.getFilterList() - - restartPager(query = "", filters = FilterList()) - } - /** * Sets the display mode. * @@ -267,50 +244,6 @@ open class CataloguePresenter( .onErrorResumeNext { Observable.just(manga) } } - /** - * Returns the last used source from preferences or the first valid source. - * - * @return a source. - */ - fun getLastUsedSource(): CatalogueSource { - val id = prefs.lastUsedCatalogueSource().get() ?: -1 - val source = sourceManager.get(id) - if (!isValidSource(source) || source !in sources) { - return sources.first { isValidSource(it) } - } - return source as CatalogueSource - } - - /** - * Checks if the given source is valid. - * - * @param source the source to check. - * @return true if the source is valid, false otherwise. - */ - open fun isValidSource(source: Source?): Boolean { - if (source == null) return false - - if (source is LoginSource) { - return source.isLogged() || - (prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "") - } - return true - } - - /** - * Returns a list of enabled sources ordered by language and name. - */ - open protected fun getEnabledSources(): List { - val languages = prefs.enabledLanguages().getOrDefault() - val hiddenCatalogues = prefs.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 - } - /** * Adds or removes a manga from the library. * @@ -370,13 +303,12 @@ open class CataloguePresenter( } is Filter.Sort -> { val group = SortGroup(it) - val subItems = it.values.mapNotNull { + val subItems = it.values.map { SortItem(it, group) } group.subItems = subItems group } - else -> null } } } @@ -407,7 +339,7 @@ open class CataloguePresenter( * @param categories the selected categories. * @param manga the manga to move. */ - fun moveMangaToCategories(manga: Manga, categories: List) { + private fun moveMangaToCategories(manga: Manga, categories: List) { val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } db.setMangaCategories(mc, listOf(manga)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt new file mode 100644 index 0000000000..0b1b822e0f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt @@ -0,0 +1,74 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.os.Bundle +import android.os.Parcelable +import android.support.v7.widget.RecyclerView +import android.util.SparseArray +import eu.davidea.flexibleadapter.FlexibleAdapter + +/** + * Adapter that holds the search cards. + * + * @param controller instance of [CatalogueSearchController]. + */ +class CatalogueSearchAdapter(val controller: CatalogueSearchController) : + FlexibleAdapter(null, controller, true) { + + /** + * Bundle where the view state of the holders is saved. + */ + private var bundle = Bundle() + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List?) { + super.onBindViewHolder(holder, position, payloads) + restoreHolderState(holder) + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + super.onViewRecycled(holder) + saveHolderState(holder, bundle) + } + + override fun onSaveInstanceState(outState: Bundle) { + val holdersBundle = Bundle() + allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) } + outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY) + } + + /** + * Saves the view state of the given holder. + * + * @param holder The holder to save. + * @param outState The bundle where the state is saved. + */ + private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) { + val key = "holder_${holder.adapterPosition}" + val holderState = SparseArray() + holder.itemView.saveHierarchyState(holderState) + outState.putSparseParcelableArray(key, holderState) + } + + /** + * Restores the view state of the given holder. + * + * @param holder The holder to restore. + */ + private fun restoreHolderState(holder: RecyclerView.ViewHolder) { + val key = "holder_${holder.adapterPosition}" + val holderState = bundle.getSparseParcelableArray(key) + if (holderState != null) { + holder.itemView.restoreHierarchyState(holderState) + bundle.remove(key) + } + } + + private companion object { + const val HOLDER_BUNDLE_KEY = "holder_bundle" + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt new file mode 100644 index 0000000000..17791f3be8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt @@ -0,0 +1,27 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.data.database.models.Manga + +/** + * Adapter that holds the manga items from search results. + * + * @param controller instance of [CatalogueSearchController]. + */ +class CatalogueSearchCardAdapter(controller: CatalogueSearchController) : + FlexibleAdapter(null, controller, true) { + + /** + * Listen for browse item clicks. + */ + val mangaClickListener: OnMangaClickListener = controller + + /** + * Listener which should be called when user clicks browse. + * Note: Should only be handled by [CatalogueSearchController] + */ + interface OnMangaClickListener { + fun onMangaClick(manga: Manga) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt new file mode 100644 index 0000000000..02c102b8fa --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.View +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.widget.StateImageViewTarget +import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.* + +class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter) + : FlexibleViewHolder(view, adapter) { + + init { + // Call onMangaClickListener when item is pressed. + itemView.setOnClickListener { + val item = adapter.getItem(adapterPosition) + if (item != null) { + adapter.mangaClickListener.onMangaClick(item.manga) + } + } + } + + fun bind(manga: Manga) { + itemView.tvTitle.text = manga.title + + setImage(manga) + } + + fun setImage(manga: Manga) { + Glide.clear(itemView.itemImage) + if (!manga.thumbnail_url.isNullOrEmpty()) { + Glide.with(itemView.context) + .load(manga) + .diskCacheStrategy(DiskCacheStrategy.SOURCE) + .centerCrop() + .skipMemoryCache(true) + .placeholder(android.R.color.transparent) + .into(StateImageViewTarget(itemView.itemImage, itemView.progress)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt new file mode 100644 index 0000000000..b721df3425 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt @@ -0,0 +1,38 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.LayoutInflater +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.util.inflate + +class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.catalogue_global_search_controller_card_item + } + + override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, + parent: ViewGroup): CatalogueSearchCardHolder { + return CatalogueSearchCardHolder(parent.inflate(layoutRes), adapter as CatalogueSearchCardAdapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder, + position: Int, payloads: List?) { + holder.bind(manga) + } + + override fun equals(other: Any?): Boolean { + if (other is CatalogueSearchCardItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id?.toInt() ?: 0 + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt new file mode 100644 index 0000000000..ea10a038ed --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt @@ -0,0 +1,171 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.SearchView +import android.view.* +import com.bluelinelabs.conductor.RouterTransaction +import com.bluelinelabs.conductor.changehandler.FadeChangeHandler +import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.manga.MangaController +import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.* + +/** + * This controller shows and manages the different search result in global search. + * This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter] + * [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search + */ +class CatalogueSearchController(private val initialQuery: String? = null) : + NucleusController(), + CatalogueSearchCardAdapter.OnMangaClickListener { + + /** + * Adapter containing search results grouped by lang. + */ + private var adapter: CatalogueSearchAdapter? = null + + /** + * Called when controller is initialized. + */ + init { + setHasOptionsMenu(true) + } + + /** + * Initiate the view with [R.layout.catalogue_global_search_controller]. + * + * @param inflater used to load the layout xml. + * @param container containing parent views. + * @return inflated view + */ + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): android.view.View { + return inflater.inflate(R.layout.catalogue_global_search_controller, container, false) + } + + /** + * Set the title of controller. + * + * @return title. + */ + override fun getTitle(): String? { + return presenter.query + } + + /** + * Create the [CatalogueSearchPresenter] used in controller. + * + * @return instance of [CatalogueSearchPresenter] + */ + override fun createPresenter(): CatalogueSearchPresenter { + return CatalogueSearchPresenter(initialQuery) + } + + /** + * Called when manga in global search is clicked, opens manga. + * + * @param manga clicked item containing manga information. + */ + override fun onMangaClick(manga: Manga) { + // Open MangaController. + router.pushController(RouterTransaction.with(MangaController(manga, true)) + .pushChangeHandler(FadeChangeHandler()) + .popChangeHandler(FadeChangeHandler())) + } + + /** + * 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_new_list, menu) + + // Initialize search menu + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.queryTextChangeEvents() + .filter { it.isSubmitted } + .subscribeUntilDestroy { + presenter.search(it.queryText().toString()) + searchItem.collapseActionView() + setTitle() // Update toolbar title + } + } + + /** + * Called when the view is created + * + * @param view view of controller + * @param savedViewState information from previous state. + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + adapter = CatalogueSearchAdapter(this) + + with(view) { + // Create recycler and set adapter. + recycler.layoutManager = LinearLayoutManager(context) + recycler.adapter = adapter + } + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + override fun onSaveViewState(view: View, outState: Bundle) { + super.onSaveViewState(view, outState) + adapter?.onSaveInstanceState(outState) + } + + override fun onRestoreViewState(view: View, savedViewState: Bundle) { + super.onRestoreViewState(view, savedViewState) + adapter?.onRestoreInstanceState(savedViewState) + } + + /** + * Returns the view holder for the given manga. + * + * @param source used to find holder containing source + * @return the holder of the manga or null if it's not bound. + */ + private fun getHolder(source: CatalogueSource): CatalogueSearchHolder? { + val adapter = adapter ?: return null + + adapter.allBoundViewHolders.forEach { holder -> + val item = adapter.getItem(holder.adapterPosition) + if (item != null && source.id == item.source.id) { + return holder as CatalogueSearchHolder + } + } + + return null + } + + /** + * Add search result to adapter. + * + * @param searchResult result of search. + */ + fun setItems(searchResult: List) { + adapter?.updateDataSet(searchResult) + } + + /** + * Called from the presenter when a manga is initialized. + * + * @param manga the initialized manga. + */ + fun onMangaInitialized(source: CatalogueSource, manga: Manga) { + getHolder(source)?.setImage(manga) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt new file mode 100644 index 0000000000..0714c4342c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt @@ -0,0 +1,100 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.support.v7.widget.LinearLayoutManager +import android.view.View +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.util.getResourceColor +import eu.kanade.tachiyomi.util.gone +import eu.kanade.tachiyomi.util.setVectorCompat +import eu.kanade.tachiyomi.util.visible +import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.* + +/** + * Holder that binds the [CatalogueSearchItem] containing catalogue cards. + * + * @param view view of [CatalogueSearchItem] + * @param adapter instance of [CatalogueSearchAdapter] + */ +class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) { + + /** + * Adapter containing manga from search results. + */ + private val mangaAdapter = CatalogueSearchCardAdapter(adapter.controller) + + private var lastBoundResults: List? = null + + init { + with(itemView) { + // Set layout horizontal. + recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + recycler.adapter = mangaAdapter + + nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp, + context.getResourceColor(android.R.attr.textColorHint)) + } + } + + /** + * Show the loading of source search result. + * + * @param item item of card. + */ + fun bind(item: CatalogueSearchItem) { + val source = item.source + val results = item.results + + with(itemView) { + // Set Title witch country code if available. + title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name + + when { + results == null -> { + progress.visible() + nothing_found.gone() + } + results.isEmpty() -> { + progress.gone() + nothing_found.visible() + } + else -> { + progress.gone() + nothing_found.gone() + } + } + if (results !== lastBoundResults) { + mangaAdapter.updateDataSet(results) + lastBoundResults = results + } + } + } + + /** + * Called from the presenter when a manga is initialized. + * + * @param manga the initialized manga. + */ + fun setImage(manga: Manga) { + getHolder(manga)?.setImage(manga) + } + + /** + * 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): CatalogueSearchCardHolder? { + mangaAdapter.allBoundViewHolders.forEach { holder -> + val item = mangaAdapter.getItem(holder.adapterPosition) + if (item != null && item.manga.id!! == manga.id!!) { + return holder as CatalogueSearchCardHolder + } + } + + return null + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt new file mode 100644 index 0000000000..265d673499 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt @@ -0,0 +1,67 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.view.LayoutInflater +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.util.inflate + +/** + * Item that contains search result information. + * + * @param source contains information about search result. + */ +class CatalogueSearchItem(val source: CatalogueSource, val results: List?) + : AbstractFlexibleItem() { + + /** + * Set view. + * + * @return id of view + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_global_search_controller_card + } + + /** + * Create view holder (see [CatalogueSearchAdapter]. + * + * @return holder of view. + */ + override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, + parent: ViewGroup): CatalogueSearchHolder { + return CatalogueSearchHolder(parent.inflate(layoutRes), adapter as CatalogueSearchAdapter) + } + + /** + * Bind item to view. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder, + position: Int, payloads: List?) { + holder.bind(this) + } + + /** + * Used to check if two items are equal. + * + * @return items are equal? + */ + override fun equals(other: Any?): Boolean { + if (other is CatalogueSearchItem) { + return source.id == other.source.id + } + return false + } + + /** + * Return hash code of item. + * + * @return hashcode + */ + override fun hashCode(): Int { + return source.id.toInt() + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt new file mode 100644 index 0000000000..243b794302 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt @@ -0,0 +1,215 @@ +package eu.kanade.tachiyomi.ui.catalogue.global_search + +import android.os.Bundle +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +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.Source +import eu.kanade.tachiyomi.source.SourceManager +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 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 [CatalogueSearchController] + * Function calls should be done from here. UI calls should be done from the controller. + * + * @param sourceManager manages the different sources. + * @param db manages the database calls. + * @param preferencesHelper manages the preference calls. + */ +class CatalogueSearchPresenter( + val initialQuery: String? = "", + val sourceManager: SourceManager = Injekt.get(), + val db: DatabaseHelper = Injekt.get(), + val preferencesHelper: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + /** + * Enabled sources. + */ + val sources by lazy { getEnabledSources() } + + /** + * Query from the view. + */ + var query = "" + private set + + /** + * Fetches the different sources by user settings. + */ + private var fetchSourcesSubscription: Subscription? = null + + /** + * Subject which fetches image of given manga. + */ + private val fetchImageSubject = PublishSubject.create, Source>>() + + /** + * Subscription for fetching images of manga. + */ + private var fetchImageSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Perform a search with previous or initial state + search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty()) + } + + override fun onDestroy() { + fetchSourcesSubscription?.unsubscribe() + fetchImageSubscription?.unsubscribe() + super.onDestroy() + } + + override fun onSave(state: Bundle) { + state.putString(CataloguePresenter::query.name, query) + super.onSave(state) + } + + /** + * Returns a list of enabled sources ordered by language and name. + * + * @return list containing enabled sources. + */ + private fun getEnabledSources(): List { + val languages = preferencesHelper.enabledLanguages().getOrDefault() + val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault() + + return sourceManager.getCatalogueSources() + .filter { it.lang in languages } + .filterNot { it is LoginSource && !it.isLogged() } + .filterNot { it.id.toString() in hiddenCatalogues } + .sortedBy { "(${it.lang}) ${it.name}" } + } + + /** + * Initiates a search for mnaga per catalogue. + * + * @param query query on which to search. + */ + fun search(query: String) { + // Return if there's nothing to do + if (this.query == query) return + + // Update query + this.query = query + + // Create image fetch subscription + initializeFetchImageSubscription() + + // Create items with the initial state + val initialItems = sources.map { CatalogueSearchItem(it, null) } + var items = initialItems + + fetchSourcesSubscription?.unsubscribe() + fetchSourcesSubscription = Observable.from(sources) + .observeOn(Schedulers.io()) + .flatMap { source -> + source.fetchSearchManga(1, query, FilterList()) + .onExceptionResumeNext(Observable.empty()) // Ignore timeouts. + .map { it.mangas.take(10) } // Get at most 10 manga from search result. + .map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga. + .doOnNext { fetchImage(it, source) } // Load manga covers. + .map { CatalogueSearchItem(source, it.map { CatalogueSearchCardItem(it) }) } + } + .observeOn(AndroidSchedulers.mainThread()) + // Update matching source with the obtained results + .map { result -> + items.map { item -> if (item.source == result.source) result else item } + } + // Update current state + .doOnNext { items = it } + // Deliver initial state + .startWith(initialItems) + .subscribeLatestCache({ view, manga -> + view.setItems(manga) + }, { _, error -> + Timber.e(error) + }) + } + + /** + * Initialize a list of manga. + * + * @param manga the list of manga to initialize. + */ + private fun fetchImage(manga: List, source: Source) { + fetchImageSubject.onNext(Pair(manga, source)) + } + + /** + * Subscribes to the initializer of manga details and updates the view if needed. + */ + private fun initializeFetchImageSubscription() { + fetchImageSubscription?.unsubscribe() + fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io()) + .flatMap { + val source = it.second + Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized } + .map { Pair(it, source) } + .concatMap { getMangaDetailsObservable(it.first, it.second) } + .map { Pair(source as CatalogueSource, it) } + + } + + .onBackpressureBuffer() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ (source, manga) -> + @Suppress("DEPRECATION") + view?.onMangaInitialized(source, manga) + }, { error -> + Timber.e(error) + }) + } + + /** + * 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, source: Source): Observable { + return source.fetchMangaDetails(manga) + .flatMap { networkManga -> + manga.copyFrom(networkManga) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + Observable.just(manga) + } + .onErrorResumeNext { Observable.just(manga) } + } + + /** + * 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt new file mode 100644 index 0000000000..d2e15169c3 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor + +/** + * Adapter that holds the catalogue cards. + * + * @param controller instance of [CatalogueMainController]. + */ +class CatalogueMainAdapter(val controller: CatalogueMainController) : + FlexibleAdapter>(null, controller, true) { + + val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) + + init { + setDisplayHeadersAtStartUp(true) + } + + /** + * Listener for browse item clicks. + */ + val browseClickListener: OnBrowseClickListener = controller + + /** + * Listener for latest item clicks. + */ + val latestClickListener: OnLatestClickListener = controller + + /** + * Listener which should be called when user clicks browse. + * Note: Should only be handled by [CatalogueMainController] + */ + interface OnBrowseClickListener { + fun onBrowseClick(position: Int) + } + + /** + * Listener which should be called when user clicks latest. + * Note: Should only be handled by [CatalogueMainController] + */ + interface OnLatestClickListener { + fun onLatestClick(position: Int) + } +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt new file mode 100644 index 0000000000..205d6fb0dc --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt @@ -0,0 +1,238 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.os.Bundle +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.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.view.* +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +/** + * This controller shows and manages the different catalogues enabled by the user. + * This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter] + * [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues. + * [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click. + * [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click + */ +class CatalogueMainController : NucleusController(), + SourceLoginDialog.Listener, + FlexibleAdapter.OnItemClickListener, + CatalogueMainAdapter.OnBrowseClickListener, + CatalogueMainAdapter.OnLatestClickListener { + + /** + * Application preferences. + */ + private val preferences: PreferencesHelper = Injekt.get() + + /** + * Adapter containing sources. + */ + private var adapter : CatalogueMainAdapter? = null + + /** + * Called when controller is initialized. + */ + init { + // Enable the option menu + setHasOptionsMenu(true) + } + + /** + * Set the title of controller. + * + * @return title. + */ + override fun getTitle(): String? { + return applicationContext?.getString(R.string.label_catalogues) + } + + /** + * Create the [CatalogueMainPresenter] used in controller. + * + * @return instance of [CatalogueMainPresenter] + */ + override fun createPresenter(): CatalogueMainPresenter { + return CatalogueMainPresenter() + } + + /** + * Initiate the view with [R.layout.catalogue_main_controller]. + * + * @param inflater used to load the layout xml. + * @param container containing parent views. + * @return inflated view. + */ + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.catalogue_main_controller, container, false) + } + + /** + * Called when the view is created + * + * @param view view of controller + * @param savedViewState information from previous state. + */ + override fun onViewCreated(view: View, savedViewState: Bundle?) { + super.onViewCreated(view, savedViewState) + + adapter = CatalogueMainAdapter(this) + + with(view) { + // Create recycler and set adapter. + recycler.layoutManager = LinearLayoutManager(context) + recycler.adapter = adapter + recycler.addItemDecoration(SourceDividerItemDecoration(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(RouterTransaction.with(controller) + .popChangeHandler(FadeChangeHandler()) + .pushChangeHandler(FadeChangeHandler())) + } + + /** + * 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((RouterTransaction.with(CatalogueSearchController(query))) + .popChangeHandler(FadeChangeHandler()) + .pushChangeHandler(FadeChangeHandler())) + } + } + + /** + * Called when an option menu item has been selected by the user. + * + * @param item The selected item. + * @return True if this event has been consumed, false if it has not. + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + // Initialize option to open catalogue settings. + R.id.action_settings -> { + router.pushController((RouterTransaction.with(SettingsSourcesController())) + .popChangeHandler(SettingsSourcesFadeChangeHandler()) + .pushChangeHandler(FadeChangeHandler())) + } + else -> return super.onOptionsItemSelected(item) + } + return true + } + + /** + * Called to update adapter containing sources. + */ + fun setSources(sources: List>) { + adapter?.updateDataSet(sources.toMutableList()) + } + + /** + * 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) + } + } + + private class SettingsSourcesFadeChangeHandler : FadeChangeHandler() +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt new file mode 100644 index 0000000000..2ce3f0a4f8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt @@ -0,0 +1,97 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.os.Bundle +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.* +import java.util.concurrent.TimeUnit + +/** + * Presenter of [CatalogueMainController] + * Function calls should be done from here. UI calls should be done from the controller. + * + * @param sourceManager manages the different sources. + * @param preferences application preferences. + */ +class CatalogueMainPresenter( + val sourceManager: SourceManager = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + /** + * Enabled sources. + */ + var sources = getEnabledSources() + + /** + * Subscription for retrieving enabled sources. + */ + private var sourceSubscription: Subscription? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Load enabled and last used sources + loadSources() + loadLastUsedSource() + } + + /** + * Unsubscribe and create a new subscription to fetch enabled sources. + */ + fun loadSources() { + sourceSubscription?.unsubscribe() + + val map = TreeMap> { d1, d2 -> d1.compareTo(d2) } + val byLang = sources.groupByTo(map, { it.lang }) + val sourceItems = byLang.flatMap { + val langItem = LangItem(it.key) + it.value.map { source -> SourceItem(source, langItem) } + } + + sourceSubscription = Observable.just(sourceItems) + .subscribeLatestCache(CatalogueMainController::setSources) + } + + private fun loadLastUsedSource() { + val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share() + + // Emit the first item immediately but delay subsequent emissions by 500ms. + Observable.merge( + sharedObs.take(1), + sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())) + .distinctUntilChanged() + .map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } } + .subscribeLatestCache(CatalogueMainController::setLastUsedSource) + } + + fun updateSources() { + sources = getEnabledSources() + loadSources() + } + + /** + * Returns a list of enabled sources ordered by language and name. + * + * @return list containing enabled sources. + */ + private fun getEnabledSources(): List { + val languages = preferences.enabledLanguages().getOrDefault() + val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault() + + return sourceManager.getCatalogueSources() + .filter { it.lang in languages } + .filterNot { it.id.toString() in hiddenCatalogues } + .sortedBy { "(${it.lang}) ${it.name}" } + + sourceManager.get(LocalSource.ID) as LocalSource + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt new file mode 100644 index 0000000000..02dcea1464 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt @@ -0,0 +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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt new file mode 100644 index 0000000000..55bdd55473 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.view.LayoutInflater +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.kanade.tachiyomi.R + +/** + * Item that contains the language header. + * + * @param code The lang code. + */ +data class LangItem(val code: String) : AbstractHeaderItem() { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, + parent: ViewGroup): LangHolder { + + return LangHolder(inflater.inflate(layoutRes, parent, false), adapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder, + position: Int, payloads: List?) { + + holder.bind(this) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt new file mode 100644 index 0000000000..bb90f5307a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt @@ -0,0 +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) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt new file mode 100644 index 0000000000..ddc8914b0e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt @@ -0,0 +1,107 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.os.Build +import android.view.View +import android.view.ViewGroup +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.online.LoginSource +import eu.kanade.tachiyomi.util.dpToPx +import eu.kanade.tachiyomi.util.getRound +import eu.kanade.tachiyomi.util.gone +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) { + + private val slice = Slice(itemView.card).apply { + setColor(adapter.cardBackground) + } + + init { + itemView.source_browse.setOnClickListener { + adapter.browseClickListener.onBrowseClick(adapterPosition) + } + + itemView.source_latest.setOnClickListener { + adapter.latestClickListener.onLatestClick(adapterPosition) + } + } + + fun bind(item: SourceItem) { + val source = item.source + with(itemView) { + setCardEdges(item) + + // Set source name + title.text = source.name + + // Set circle letter image. + post { + image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) + } + + // If source is login, show only login option + if (source is LoginSource && !source.isLogged()) { + source_browse.setText(R.string.login) + source_latest.gone() + } else { + source_browse.setText(R.string.browse) + source_latest.visible() + } + } + } + + private fun setCardEdges(item: SourceItem) { + // Position of this item in its header. Defaults to 0 when header is null. + var position = 0 + + // Number of items in the header of this item. Defaults to 1 when header is null. + var count = 1 + + if (item.header != null) { + val sectionItems = mAdapter.getSectionItems(item.header) + position = sectionItems.indexOf(item) + count = sectionItems.size + } + + when { + // Only one item in the card + count == 1 -> applySlice(2f, false, false, true, true) + // First item of the card + position == 0 -> applySlice(2f, false, true, true, false) + // Last item of the card + position == count - 1 -> applySlice(2f, true, false, false, true) + // Middle item + else -> applySlice(0f, false, false, false, false) + } + } + + private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, + topShadow: Boolean, bottomShadow: Boolean) { + + slice.setRadius(radius) + slice.showLeftTopRect(topRect) + slice.showRightTopRect(topRect) + slice.showLeftBottomRect(bottomRect) + slice.showRightBottomRect(bottomRect) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + slice.showTopEdgeShadow(topShadow) + slice.showBottomEdgeShadow(bottomShadow) + } + setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) + } + + private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { + val v = itemView.card + if (v.layoutParams is ViewGroup.MarginLayoutParams) { + val p = v.layoutParams as ViewGroup.MarginLayoutParams + p.setMargins(left, top, right, bottom) + } + } + + companion object { + val margin = 8.dpToPx + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt new file mode 100644 index 0000000000..14593ab076 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt @@ -0,0 +1,45 @@ +package eu.kanade.tachiyomi.ui.catalogue.main + +import android.view.LayoutInflater +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractSectionableItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.CatalogueSource + +/** + * Item that contains source information. + * + * @param source Instance of [CatalogueSource] containing source information. + * @param header The header for this item. + */ +data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) : + AbstractSectionableItem(header) { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card_item + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater, + parent: ViewGroup): SourceHolder { + + val view = inflater.inflate(layoutRes, parent, false) + return SourceHolder(view, adapter as CatalogueMainAdapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder, + position: Int, payloads: List?) { + + holder.bind(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt index 906bbb9107..0dd3c1fa76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt @@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.util.ColorGenerator import eu.davidea.viewholders.FlexibleViewHolder import eu.kanade.tachiyomi.data.database.models.Category +import eu.kanade.tachiyomi.util.getRound import kotlinx.android.synthetic.main.categories_item.view.* /** @@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol // Update circle letter image. itemView.post { - itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase())) + itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false)) } } - /** - * Returns circle letter image. - * - * @param text The first letter of string. - */ - private fun getRound(text: String): TextDrawable { - val size = Math.min(itemView.image.width, itemView.image.height) - return TextDrawable.builder() - .beginConfig() - .width(size) - .height(size) - .textColor(Color.WHITE) - .useFont(Typeface.DEFAULT) - .endConfig() - .buildRound(text, ColorGenerator.MATERIAL.getColor(text)) - } - /** * Called when an item is released. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt index 730b5e9910..072980607e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt @@ -1,19 +1,25 @@ 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 /** - * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment. + * Controller that shows the latest manga from the catalogue. Inherit [CatalogueController]. */ -class LatestUpdatesController : 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() + return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY)) } override fun onPrepareOptionsMenu(menu: Menu) { @@ -30,4 +36,4 @@ class LatestUpdatesController : CatalogueController() { } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt index 924425b62b..2e0ea07fed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt @@ -1,7 +1,5 @@ package eu.kanade.tachiyomi.ui.latest_updates -import eu.kanade.tachiyomi.source.CatalogueSource -import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter import eu.kanade.tachiyomi.ui.catalogue.Pager @@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager /** * Presenter of [LatestUpdatesController]. Inherit CataloguePresenter. */ -class LatestUpdatesPresenter : CataloguePresenter() { +class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) { override fun createPager(query: String, filters: FilterList): Pager { return LatestUpdatesPager(source) } - override fun getEnabledSources(): List { - return super.getEnabledSources().filter { it.supportsLatest } - } - - override fun isValidSource(source: Source?): Boolean { - return super.isValidSource(source) && (source as CatalogueSource).supportsLatest - } - } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index d6bf1d2331..7db5474c41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -18,9 +18,8 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.TabbedController -import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController import eu.kanade.tachiyomi.ui.download.DownloadController -import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController @@ -84,8 +83,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(CatalogueController(), id) - R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id) + R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id) R.id.nav_drawer_downloads -> { router.pushController(RouterTransaction.with(DownloadController()) .pushChangeHandler(FadeChangeHandler()) @@ -250,4 +248,4 @@ class MainActivity : BaseActivity() { const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA" } -} \ No newline at end of file +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 4953429fb8..c548bb849b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -30,12 +30,6 @@ class SettingsMainController : SettingsController() { titleRes = R.string.pref_category_downloads onClick { navigateTo(SettingsDownloadController()) } } - preference { - iconRes = R.drawable.ic_language_black_24dp - iconTint = tintColor - titleRes = R.string.pref_category_sources - onClick { navigateTo(SettingsSourcesController()) } - } preference { iconRes = R.drawable.ic_sync_black_24dp iconTint = tintColor diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt index c7982d0d88..25f847b725 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt @@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.setting import android.graphics.drawable.Drawable import android.support.v7.preference.PreferenceGroup import android.support.v7.preference.PreferenceScreen +import com.bluelinelabs.conductor.ControllerChangeHandler +import com.bluelinelabs.conductor.ControllerChangeType import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.source.SourceManager diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt index e37667f76d..f2063e6751 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt @@ -105,7 +105,7 @@ val Context.powerManager: PowerManager * * @param intent intent that contains broadcast information */ -fun Context.sendLocalBroadcast(intent:Intent){ +fun Context.sendLocalBroadcast(intent: Intent) { LocalBroadcastManager.getInstance(this).sendBroadcast(intent) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt index 912f05e806..0bd1ce9a21 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt @@ -4,9 +4,12 @@ package eu.kanade.tachiyomi.util import android.graphics.Color import android.graphics.Point +import android.graphics.Typeface import android.support.design.widget.Snackbar import android.view.View import android.widget.TextView +import com.amulyakhare.textdrawable.TextDrawable +import com.amulyakhare.textdrawable.util.ColorGenerator /** * Returns coordinates of view. @@ -43,3 +46,21 @@ inline fun View.invisible() { inline fun View.gone() { visibility = View.GONE } + +/** + * Returns a TextDrawable determined by input + * + * @param text text of [TextDrawable] + * @param random random color + */ +fun View.getRound(text: String, random : Boolean = true): TextDrawable { + val size = Math.min(this.width, this.height) + return TextDrawable.builder() + .beginConfig() + .width(size) + .height(size) + .textColor(Color.WHITE) + .useFont(Typeface.DEFAULT) + .endConfig() + .buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text)) +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml index 4ad2729afe..5eab2ca696 100644 --- a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml +++ b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml @@ -18,6 +18,4 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/library_item_selector_dark.xml b/app/src/main/res/drawable-v21/library_item_selector_dark.xml index e78c6ec16e..82a72da4aa 100644 --- a/app/src/main/res/drawable-v21/library_item_selector_dark.xml +++ b/app/src/main/res/drawable-v21/library_item_selector_dark.xml @@ -18,6 +18,4 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/library_item_selector_light.xml b/app/src/main/res/drawable-v21/library_item_selector_light.xml index c85ee39136..1f2e8bf89c 100644 --- a/app/src/main/res/drawable-v21/library_item_selector_light.xml +++ b/app/src/main/res/drawable-v21/library_item_selector_light.xml @@ -18,6 +18,4 @@ - - - + \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml index 96494d93ab..0fce81a343 100644 --- a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml +++ b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml @@ -1,6 +1,6 @@ + android:color="@color/rippleColorDark"> diff --git a/app/src/main/res/drawable-v21/list_item_selector_dark.xml b/app/src/main/res/drawable-v21/list_item_selector_dark.xml index 07ed74dd40..07b9ef6d5b 100644 --- a/app/src/main/res/drawable-v21/list_item_selector_dark.xml +++ b/app/src/main/res/drawable-v21/list_item_selector_dark.xml @@ -1,6 +1,6 @@ + android:color="@color/rippleColorDark"> diff --git a/app/src/main/res/drawable-v21/list_item_selector_light.xml b/app/src/main/res/drawable-v21/list_item_selector_light.xml index 692d94f2bb..942446ef06 100644 --- a/app/src/main/res/drawable-v21/list_item_selector_light.xml +++ b/app/src/main/res/drawable-v21/list_item_selector_light.xml @@ -1,6 +1,6 @@ + android:color="@color/rippleColorLight"> diff --git a/app/src/main/res/drawable-v21/list_item_selector_trans.xml b/app/src/main/res/drawable-v21/list_item_selector_trans.xml new file mode 100644 index 0000000000..2c8d1001b3 --- /dev/null +++ b/app/src/main/res/drawable-v21/list_item_selector_trans.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_search_black_112dp.xml b/app/src/main/res/drawable/ic_search_black_112dp.xml new file mode 100644 index 0000000000..05705a6079 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_black_112dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/library_item_selector_amoled.xml b/app/src/main/res/drawable/library_item_selector_amoled.xml index 92cb0db941..1cf05bdc96 100644 --- a/app/src/main/res/drawable/library_item_selector_amoled.xml +++ b/app/src/main/res/drawable/library_item_selector_amoled.xml @@ -1,10 +1,10 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_selector_dark.xml b/app/src/main/res/drawable/library_item_selector_dark.xml index 73de4df076..9880c4b38c 100644 --- a/app/src/main/res/drawable/library_item_selector_dark.xml +++ b/app/src/main/res/drawable/library_item_selector_dark.xml @@ -1,10 +1,10 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/library_item_selector_light.xml b/app/src/main/res/drawable/library_item_selector_light.xml index 9273e00fe3..70f7b85b47 100644 --- a/app/src/main/res/drawable/library_item_selector_light.xml +++ b/app/src/main/res/drawable/library_item_selector_light.xml @@ -1,19 +1,10 @@ - - - - - - - - - + xmlns:android="http://schemas.android.com/apk/res/android"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/list_item_selector_amoled.xml b/app/src/main/res/drawable/list_item_selector_amoled.xml index e573c82bb5..9bbf565786 100644 --- a/app/src/main/res/drawable/list_item_selector_amoled.xml +++ b/app/src/main/res/drawable/list_item_selector_amoled.xml @@ -1,6 +1,6 @@ + android:exitFadeDuration="@android:integer/config_longAnimTime"> diff --git a/app/src/main/res/drawable/list_item_selector_dark.xml b/app/src/main/res/drawable/list_item_selector_dark.xml index dd07798858..60034f8183 100644 --- a/app/src/main/res/drawable/list_item_selector_dark.xml +++ b/app/src/main/res/drawable/list_item_selector_dark.xml @@ -1,6 +1,6 @@ + android:exitFadeDuration="@android:integer/config_longAnimTime"> diff --git a/app/src/main/res/drawable/list_item_selector_light.xml b/app/src/main/res/drawable/list_item_selector_light.xml index 73e54c92fe..92bed9fc90 100644 --- a/app/src/main/res/drawable/list_item_selector_light.xml +++ b/app/src/main/res/drawable/list_item_selector_light.xml @@ -1,15 +1,6 @@ - - - - - - - - - + android:exitFadeDuration="@android:integer/config_longAnimTime"> diff --git a/app/src/main/res/drawable/list_item_selector_trans.xml b/app/src/main/res/drawable/list_item_selector_trans.xml new file mode 100644 index 0000000000..f61aa8a0d0 --- /dev/null +++ b/app/src/main/res/drawable/list_item_selector_trans.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/text_button.xml b/app/src/main/res/drawable/text_button.xml new file mode 100644 index 0000000000..ef5c24c567 --- /dev/null +++ b/app/src/main/res/drawable/text_button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_controller.xml b/app/src/main/res/layout/catalogue_controller.xml index 48d8601ba8..17ba20e10b 100644 --- a/app/src/main/res/layout/catalogue_controller.xml +++ b/app/src/main/res/layout/catalogue_controller.xml @@ -6,12 +6,12 @@ android:layout_height="match_parent"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true" + android:orientation="vertical" + android:id="@+id/catalogue_view" + tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController"> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_global_search_controller_card.xml b/app/src/main/res/layout/catalogue_global_search_controller_card.xml new file mode 100644 index 0000000000..db1a39ca06 --- /dev/null +++ b/app/src/main/res/layout/catalogue_global_search_controller_card.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_global_search_controller_card_item.xml b/app/src/main/res/layout/catalogue_global_search_controller_card_item.xml new file mode 100644 index 0000000000..396ff77794 --- /dev/null +++ b/app/src/main/res/layout/catalogue_global_search_controller_card_item.xml @@ -0,0 +1,55 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_grid_item.xml b/app/src/main/res/layout/catalogue_grid_item.xml index ee76f80b13..e475733d46 100644 --- a/app/src/main/res/layout/catalogue_grid_item.xml +++ b/app/src/main/res/layout/catalogue_grid_item.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?attr/selectable_library_drawable"> + android:background="?selectable_library_drawable"> + android:paddingStart="@dimen/material_component_lists_icon_left_padding"/> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_main_controller_card.xml b/app/src/main/res/layout/catalogue_main_controller_card.xml new file mode 100644 index 0000000000..aec409b0a0 --- /dev/null +++ b/app/src/main/res/layout/catalogue_main_controller_card.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_main_controller_card_item.xml b/app/src/main/res/layout/catalogue_main_controller_card_item.xml new file mode 100644 index 0000000000..4f996dc84a --- /dev/null +++ b/app/src/main/res/layout/catalogue_main_controller_card_item.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_recycler_autofit.xml b/app/src/main/res/layout/catalogue_recycler_autofit.xml index a6e1037939..5ba8ec8ddf 100644 --- a/app/src/main/res/layout/catalogue_recycler_autofit.xml +++ b/app/src/main/res/layout/catalogue_recycler_autofit.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/catalogue_grid" - style="@style/Theme.Widget.GridView" + style="@style/Theme.Widget.GridView.Catalogue" android:layout_width="match_parent" android:layout_height="match_parent" android:columnWidth="140dp" diff --git a/app/src/main/res/layout/categories_item.xml b/app/src/main/res/layout/categories_item.xml index 42cbe12af2..681f229a0e 100644 --- a/app/src/main/res/layout/categories_item.xml +++ b/app/src/main/res/layout/categories_item.xml @@ -15,7 +15,8 @@ android:paddingLeft="@dimen/material_component_lists_icon_left_padding" android:paddingStart="@dimen/material_component_lists_icon_left_padding" android:paddingRight="0dp" - android:paddingEnd="0dp"/> + android:paddingEnd="0dp" + tools:src="@mipmap/ic_launcher_round"/> + + + + + diff --git a/app/src/main/res/menu/catalogue_new_list.xml b/app/src/main/res/menu/catalogue_new_list.xml new file mode 100644 index 0000000000..a528ba8e45 --- /dev/null +++ b/app/src/main/res/menu/catalogue_new_list.xml @@ -0,0 +1,11 @@ +

+ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8fed988aa8..47fcc3a5a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -34,6 +34,7 @@ Last read Last updated Search + Global search Select all Mark as read Mark as unread @@ -85,6 +86,8 @@ Open log Create Restore + Open + Login Deleting… @@ -276,8 +279,13 @@ Please enable at least one valid source No more results Local manga + Other Default can\'t be selected with other categories The manga has been added to your library + Global search… + No results found! + Latest + Browse This manga was removed from the database! @@ -430,5 +438,4 @@ No wifi connection available No network connection available Download paused - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 66e4210d42..a70b29ba3c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -4,7 +4,7 @@ - + + @@ -105,6 +109,10 @@ 20sp + + @@ -130,7 +138,7 @@ - @@ -161,21 +175,24 @@ + + - - + -