Bläddra i källkod

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
Bram van de Kerkhof 7 år sedan
förälder
incheckning
54c8b3ef29
61 ändrade filer med 1851 tillägg och 261 borttagningar
  1. 2 1
      app/build.gradle
  2. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
  3. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
  4. 25 63
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
  5. 13 81
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
  6. 74 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt
  7. 27 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt
  8. 43 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt
  9. 38 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt
  10. 171 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt
  11. 100 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt
  12. 67 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt
  13. 215 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt
  14. 48 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt
  15. 238 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt
  16. 97 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt
  17. 21 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt
  18. 41 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt
  19. 47 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt
  20. 107 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt
  21. 45 0
      app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt
  22. 2 18
      app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
  23. 10 4
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
  24. 1 11
      app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
  25. 3 5
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  26. 0 6
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
  27. 2 0
      app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt
  28. 1 1
      app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt
  29. 21 0
      app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt
  30. 1 3
      app/src/main/res/drawable-v21/library_item_selector_amoled.xml
  31. 1 3
      app/src/main/res/drawable-v21/library_item_selector_dark.xml
  32. 1 3
      app/src/main/res/drawable-v21/library_item_selector_light.xml
  33. 1 1
      app/src/main/res/drawable-v21/list_item_selector_amoled.xml
  34. 1 1
      app/src/main/res/drawable-v21/list_item_selector_dark.xml
  35. 1 1
      app/src/main/res/drawable-v21/list_item_selector_light.xml
  36. 6 0
      app/src/main/res/drawable-v21/list_item_selector_trans.xml
  37. 9 0
      app/src/main/res/drawable/ic_search_black_112dp.xml
  38. 2 2
      app/src/main/res/drawable/library_item_selector_amoled.xml
  39. 2 2
      app/src/main/res/drawable/library_item_selector_dark.xml
  40. 2 11
      app/src/main/res/drawable/library_item_selector_light.xml
  41. 1 1
      app/src/main/res/drawable/list_item_selector_amoled.xml
  42. 1 1
      app/src/main/res/drawable/list_item_selector_dark.xml
  43. 1 10
      app/src/main/res/drawable/list_item_selector_light.xml
  44. 10 0
      app/src/main/res/drawable/list_item_selector_trans.xml
  45. 15 0
      app/src/main/res/drawable/text_button.xml
  46. 6 6
      app/src/main/res/layout/catalogue_controller.xml
  47. 14 0
      app/src/main/res/layout/catalogue_global_search_controller.xml
  48. 83 0
      app/src/main/res/layout/catalogue_global_search_controller_card.xml
  49. 55 0
      app/src/main/res/layout/catalogue_global_search_controller_card_item.xml
  50. 1 1
      app/src/main/res/layout/catalogue_grid_item.xml
  51. 1 2
      app/src/main/res/layout/catalogue_list_item.xml
  52. 14 0
      app/src/main/res/layout/catalogue_main_controller.xml
  53. 18 0
      app/src/main/res/layout/catalogue_main_controller_card.xml
  54. 72 0
      app/src/main/res/layout/catalogue_main_controller_card_item.xml
  55. 1 1
      app/src/main/res/layout/catalogue_recycler_autofit.xml
  56. 2 1
      app/src/main/res/layout/categories_item.xml
  57. 1 1
      app/src/main/res/layout/library_grid_recycler.xml
  58. 16 0
      app/src/main/res/menu/catalogue_main.xml
  59. 11 0
      app/src/main/res/menu/catalogue_new_list.xml
  60. 8 1
      app/src/main/res/values/strings.xml
  61. 32 17
      app/src/main/res/values/styles.xml

+ 2 - 1
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 {
             }
         }
     }
-}
+}

+ 1 - 1
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) {

+ 1 - 1
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<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(),
+abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
         PresenterFactory<P> {
 
     private val delegate = NucleusConductorDelegate(this)

+ 25 - 63
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<CataloguePresenter>(bundle),
         SecondaryDrawerController,
         FlexibleAdapter.OnItemClickListener,
@@ -51,6 +47,10 @@ open class CatalogueController(bundle: Bundle? = null) :
         FlexibleAdapter.EndlessScrollListener<ProgressItem>,
         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<IFlexible<*>>? = 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
 
-    private var drawerListener: DrawerLayout.DrawerListener? = null
-
     /**
-     * Query of the search box.
+     * Drawer listener to allow swipe only for closing the drawer.
      */
-    private val query: String
-        get() = presenter.query
-
-    /**
-     * Selected index of the spinner (selected source).
-     */
-    private var selectedIndex: Int = 0
+    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 {
@@ -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<Int> {
         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"
+    }
+
 }

+ 13 - 81
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<CatalogueController>() {
 
     /**
-     * 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<CatalogueSource> {
-        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<Category>) {
+    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))
     }

+ 74 - 0
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<CatalogueSearchItem>(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<Any?>?) {
+        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<Parcelable>()
+        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<Parcelable>(key)
+        if (holderState != null) {
+            holder.itemView.restoreHierarchyState(holderState)
+            bundle.remove(key)
+        }
+    }
+
+    private companion object {
+        const val HOLDER_BUNDLE_KEY = "holder_bundle"
+    }
+}

+ 27 - 0
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<CatalogueSearchCardItem>(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)
+    }
+
+}

+ 43 - 0
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))
+        }
+    }
+
+}

+ 38 - 0
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<CatalogueSearchCardHolder>() {
+
+    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<Any?>?) {
+        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
+    }
+
+}

+ 171 - 0
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<CatalogueSearchPresenter>(),
+        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<CatalogueSearchItem>) {
+        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)
+    }
+
+}

+ 100 - 0
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<CatalogueSearchCardItem>? = 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
+    }
+
+}

+ 67 - 0
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<CatalogueSearchCardItem>?)
+    : AbstractFlexibleItem<CatalogueSearchHolder>() {
+
+    /**
+     * 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<Any?>?) {
+        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()
+    }
+
+}

+ 215 - 0
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<CatalogueSearchController>() {
+
+    /**
+     * 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<Pair<List<Manga>, 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<CatalogueSource> {
+        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<Manga>, 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<Manga> {
+        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
+    }
+}

+ 48 - 0
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<IFlexible<*>>(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)
+    }
+}
+

+ 238 - 0
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<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
+     * @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<IFlexible<*>>) {
+        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()
+}

+ 97 - 0
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<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 -> 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
+    }
+}

+ 21 - 0
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()
+            }
+        }
+    }
+}

+ 41 - 0
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<LangHolder>() {
+
+    /**
+     * 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<Any?>?) {
+
+        holder.bind(this)
+    }
+
+}

+ 47 - 0
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)
+    }
+}

+ 107 - 0
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
+    }
+}

+ 45 - 0
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<SourceHolder, LangItem>(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<Any?>?) {
+
+        holder.bind(this)
+    }
+
+}

+ 2 - 18
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.
      *

+ 10 - 4
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() {
 
     }
 
-}
+}

+ 1 - 11
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<CatalogueSource> {
-        return super.getEnabledSources().filter { it.supportsLatest }
-    }
-
-    override fun isValidSource(source: Source?): Boolean {
-        return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
-    }
-
 }

+ 3 - 5
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"
     }
 
-}
+}

+ 0 - 6
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

+ 2 - 0
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

+ 1 - 1
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)
 }
 

+ 21 - 0
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))
+}

+ 1 - 3
app/src/main/res/drawable-v21/library_item_selector_amoled.xml

@@ -18,6 +18,4 @@
             </item>
         </selector>
     </item>
-
-
-</ripple>
+</ripple>

+ 1 - 3
app/src/main/res/drawable-v21/library_item_selector_dark.xml

@@ -18,6 +18,4 @@
             </item>
         </selector>
     </item>
-
-
-</ripple>
+</ripple>

+ 1 - 3
app/src/main/res/drawable-v21/library_item_selector_light.xml

@@ -18,6 +18,4 @@
             </item>
         </selector>
     </item>
-
-
-</ripple>
+</ripple>

+ 1 - 1
app/src/main/res/drawable-v21/list_item_selector_amoled.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="@color/rippleColorDark">
+    android:color="@color/rippleColorDark">
     <item>
         <selector>
             <item android:state_selected="true">

+ 1 - 1
app/src/main/res/drawable-v21/list_item_selector_dark.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="@color/rippleColorDark">
+    android:color="@color/rippleColorDark">
     <item>
         <selector>
             <item android:state_selected="true">

+ 1 - 1
app/src/main/res/drawable-v21/list_item_selector_light.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
-        android:color="@color/rippleColorLight">
+    android:color="@color/rippleColorLight">
     <item>
         <selector>
             <item android:state_selected="true">

+ 6 - 0
app/src/main/res/drawable-v21/list_item_selector_trans.xml

@@ -0,0 +1,6 @@
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <color android:color="@android:color/white" />
+    </item>
+</ripple>

+ 9 - 0
app/src/main/res/drawable/ic_search_black_112dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="112dp"
+    android:height="112dp"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z" />
+</vector>

+ 2 - 2
app/src/main/res/drawable/library_item_selector_amoled.xml

@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <selector android:exitFadeDuration="@android:integer/config_longAnimTime"
-          xmlns:android="http://schemas.android.com/apk/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:state_focused="true" android:drawable="@color/selectorColorDark"/>
     <item android:state_pressed="true" android:drawable="@color/selectorColorDark"/>
     <item android:state_activated="true" android:drawable="@color/selectorColorDark"/>
     <item android:drawable="@color/md_black_1000"/>
 
-</selector>
+</selector>

+ 2 - 2
app/src/main/res/drawable/library_item_selector_dark.xml

@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <selector android:exitFadeDuration="@android:integer/config_longAnimTime"
-          xmlns:android="http://schemas.android.com/apk/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:state_focused="true" android:drawable="@color/selectorColorDark"/>
     <item android:state_pressed="true" android:drawable="@color/selectorColorDark"/>
     <item android:state_activated="true" android:drawable="@color/selectorColorDark"/>
     <item android:drawable="@color/backgroundDark"/>
 
-</selector>
+</selector>

+ 2 - 11
app/src/main/res/drawable/library_item_selector_light.xml

@@ -1,19 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--<selector android:exitFadeDuration="@android:integer/config_longAnimTime"-->
-<!--xmlns:android="http://schemas.android.com/apk/res/android">-->
-
-<!--<item android:state_focused="true" android:drawable="?attr/colorAccent"/>-->
-<!--<item android:state_pressed="true" android:drawable="?attr/colorAccent"/>-->
-<!--<item android:state_activated="true" android:drawable="?attr/colorAccent"/>-->
-<!--<item android:drawable="?android:attr/colorBackground"/>-->
-<!--</selector>-->
-
 <selector android:exitFadeDuration="@android:integer/config_longAnimTime"
-          xmlns:android="http://schemas.android.com/apk/res/android">
+    xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:state_focused="true" android:drawable="@color/selectorColorLight"/>
     <item android:state_pressed="true" android:drawable="@color/selectorColorLight"/>
     <item android:state_activated="true" android:drawable="@color/selectorColorLight"/>
     <item android:drawable="@color/backgroundLight"/>
 
-</selector>
+</selector>

+ 1 - 1
app/src/main/res/drawable/list_item_selector_amoled.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
-          android:exitFadeDuration="@android:integer/config_longAnimTime">
+    android:exitFadeDuration="@android:integer/config_longAnimTime">
 
     <item android:drawable="@color/rippleColorDark" android:state_focused="true"/>
     <item android:drawable="@color/rippleColorDark" android:state_pressed="true"/>

+ 1 - 1
app/src/main/res/drawable/list_item_selector_dark.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
-          android:exitFadeDuration="@android:integer/config_longAnimTime">
+    android:exitFadeDuration="@android:integer/config_longAnimTime">
 
     <item android:drawable="@color/rippleColorDark" android:state_focused="true"/>
     <item android:drawable="@color/rippleColorDark" android:state_pressed="true"/>

+ 1 - 10
app/src/main/res/drawable/list_item_selector_light.xml

@@ -1,15 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!--<selector android:exitFadeDuration="@android:integer/config_longAnimTime"-->
-          <!--xmlns:android="http://schemas.android.com/apk/res/android">-->
-
-    <!--<item android:state_focused="true" android:drawable="?attr/colorAccent"/>-->
-    <!--<item android:state_pressed="true" android:drawable="?attr/colorAccent"/>-->
-    <!--<item android:state_activated="true" android:drawable="?attr/colorAccent"/>-->
-    <!--<item android:drawable="?android:attr/colorBackground"/>-->
-<!--</selector>-->
-
 <selector xmlns:android="http://schemas.android.com/apk/res/android"
-          android:exitFadeDuration="@android:integer/config_longAnimTime">
+    android:exitFadeDuration="@android:integer/config_longAnimTime">
 
     <item android:drawable="@color/rippleColorLight" android:state_focused="true"/>
     <item android:drawable="@color/rippleColorLight" android:state_pressed="true"/>

+ 10 - 0
app/src/main/res/drawable/list_item_selector_trans.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:exitFadeDuration="@android:integer/config_longAnimTime">
+
+    <item android:drawable="@color/rippleColorLight" android:state_focused="true"/>
+    <item android:drawable="@color/rippleColorLight" android:state_pressed="true"/>
+    <item android:drawable="@color/rippleColorLight" android:state_activated="true"/>
+    <item android:drawable="@android:color/transparent"/>
+
+</selector>

+ 15 - 0
app/src/main/res/drawable/text_button.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" >
+    <stroke
+        android:width="1dp"
+        android:color="?attr/colorAccent" />
+
+    <solid android:color="?attr/cardBackgroundColor" />
+
+    <padding
+        android:left="1dp"
+        android:right="1dp"
+        android:top="1dp" />
+
+    <corners android:radius="5dp" />
+</shape>

+ 6 - 6
app/src/main/res/layout/catalogue_controller.xml

@@ -6,12 +6,12 @@
     android:layout_height="match_parent">
 
     <LinearLayout
-          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">
+        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">
 
         <ProgressBar
             android:id="@+id/progress"

+ 14 - 0
app/src/main/res/layout/catalogue_global_search_controller.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+xmlns:tools="http://schemas.android.com/tools"
+android:layout_width="match_parent"
+android:layout_height="wrap_content">
+
+<android.support.v7.widget.RecyclerView
+    android:id="@+id/recycler"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingBottom="4dp"
+    android:paddingTop="4dp"
+    tools:listitem="@layout/catalogue_global_search_controller_card" />
+</FrameLayout>

+ 83 - 0
app/src/main/res/layout/catalogue_global_search_controller_card.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <TextView
+        android:id="@+id/title"
+        style="@style/TextAppearance.Regular.SubHeading"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:padding="@dimen/material_component_text_fields_padding_above_and_below_label"
+        app:layout_constraintBottom_toTopOf="@+id/source_card"
+        app:layout_constraintHeight_default="wrap"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Title" />
+
+    <android.support.v7.widget.CardView
+        android:id="@+id/source_card"
+        style="@style/Theme.Widget.CardView.Item"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:minHeight="144dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintHeight_default="wrap"
+        app:layout_constraintStart_toStartOf="parent">
+
+        <ProgressBar
+            android:id="@+id/progress"
+            style="?android:attr/progressBarStyleSmall"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center" />
+
+        <android.support.constraint.ConstraintLayout
+            android:id="@+id/nothing_found"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:visibility="gone">
+
+            <ImageView
+                android:id="@+id/nothing_found_icon"
+                android:layout_width="112dp"
+                android:layout_height="112dp"
+                android:scaleType="fitCenter"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:ignore="ContentDescription"
+                tools:src="@mipmap/ic_launcher" />
+
+            <TextView
+                android:id="@+id/nothing_found_text"
+                style="@style/TextAppearance.Regular.Caption.Hint"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="0dp"
+                android:ellipsize="end"
+                android:gravity="center"
+                android:maxLines="1"
+                android:paddingBottom="8dp"
+                android:text="@string/no_results"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/nothing_found_icon" />
+
+        </android.support.constraint.ConstraintLayout>
+
+        <android.support.v7.widget.RecyclerView
+            android:id="@+id/recycler"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:paddingEnd="4dp"
+            android:paddingStart="4dp"
+            tools:listitem="@layout/catalogue_global_search_controller_card_item" />
+    </android.support.v7.widget.CardView>
+</android.support.constraint.ConstraintLayout>

+ 55 - 0
app/src/main/res/layout/catalogue_global_search_controller_card_item.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="?attr/selectable_list_drawable"
+    android:orientation="vertical"
+    android:paddingBottom="8dp"
+    android:paddingEnd="4dp"
+    android:paddingStart="4dp"
+    android:paddingTop="8dp">
+
+    <ProgressBar
+        android:id="@+id/progress"
+        style="?android:attr/progressBarStyleSmall"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:visibility="gone"
+        app:layout_constraintHeight_default="wrap"
+        app:layout_constraintWidth_default="wrap"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <ImageView
+        android:id="@+id/itemImage"
+        android:layout_width="112dp"
+        android:layout_height="112dp"
+        android:paddingBottom="8dp"
+        android:scaleType="fitCenter"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="ContentDescription"
+        tools:src="@mipmap/ic_launcher" />
+
+    <TextView
+        android:id="@+id/tvTitle"
+        style="@style/TextAppearance.Regular.Caption"
+        android:layout_width="104dp"
+        android:layout_height="0dp"
+        android:layout_marginTop="0dp"
+        android:ellipsize="end"
+        android:gravity="center"
+        android:maxLines="1"
+        app:layout_constraintHeight_default="wrap"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/itemImage"
+        tools:text="Sample title" />
+
+</android.support.constraint.ConstraintLayout>

+ 1 - 1
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">
 
     <FrameLayout
         android:layout_width="wrap_content"

+ 1 - 2
app/src/main/res/layout/catalogue_list_item.xml

@@ -14,8 +14,7 @@
         android:paddingEnd="0dp"
         android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
         android:paddingRight="0dp"
-        android:paddingStart="@dimen/material_component_lists_icon_left_padding"
-        tools:src="@drawable/icon"/>
+        android:paddingStart="@dimen/material_component_lists_icon_left_padding"/>
 
     <RelativeLayout
         android:layout_width="match_parent"

+ 14 - 0
app/src/main/res/layout/catalogue_main_controller.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/recycler"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        tools:listitem="@layout/catalogue_main_controller_card" />
+
+</FrameLayout>

+ 18 - 0
app/src/main/res/layout/catalogue_main_controller_card.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <TextView
+        android:id="@+id/title"
+        style="@style/TextAppearance.Regular.SubHeading"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:paddingTop="8dp"
+        android:paddingBottom="8dp"
+        android:paddingLeft="@dimen/material_component_text_fields_padding_above_and_below_label"
+        tools:text="Title" />
+
+</FrameLayout>

+ 72 - 0
app/src/main/res/layout/catalogue_main_controller_card_item.xml

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <android.support.constraint.ConstraintLayout
+        android:id="@+id/card"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/material_component_lists_two_line_height"
+        android:background="?attr/selectable_list_drawable">
+
+        <ImageView
+            android:id="@+id/image"
+            android:layout_width="48dp"
+            android:layout_height="56dp"
+            android:clickable="true"
+            android:paddingLeft="8dp"
+            android:paddingStart="8dp"
+            android:paddingRight="0dp"
+            android:paddingEnd="0dp"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintLeft_toLeftOf="parent"
+            tools:src="@mipmap/ic_launcher_round"/>
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:maxLines="1"
+            android:paddingLeft="16dp"
+            android:paddingStart="16dp"
+            android:paddingRight="8dp"
+            android:paddingEnd="8dp"
+            android:ellipsize="end"
+            android:textAppearance="@style/TextAppearance.Regular.SubHeading"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintLeft_toRightOf="@+id/image"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintRight_toLeftOf="@+id/source_latest"
+            tools:text="Source title"/>
+
+        <TextView
+            android:id="@+id/source_latest"
+            style="@style/TextAppearance.Medium.Button"
+            android:background="@drawable/list_item_selector_trans"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/latest"
+            android:padding="@dimen/material_component_dialogs_padding_around_buttons"
+            app:layout_constraintRight_toLeftOf="@+id/source_browse"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintTop_toTopOf="parent"/>
+
+        <TextView
+            android:id="@+id/source_browse"
+            style="@style/TextAppearance.Medium.Button"
+            android:background="@drawable/list_item_selector_trans"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/browse"
+            android:padding="@dimen/material_component_dialogs_padding_around_buttons"
+            app:layout_constraintRight_toRightOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintTop_toTopOf="parent"/>
+
+    </android.support.constraint.ConstraintLayout>
+
+</FrameLayout>

+ 1 - 1
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"

+ 2 - 1
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"/>
 
     <TextView
         android:id="@+id/title"

+ 1 - 1
app/src/main/res/layout/library_grid_recycler.xml

@@ -3,7 +3,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/library_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"

+ 16 - 0
app/src/main/res/menu/catalogue_main.xml

@@ -0,0 +1,16 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools" tools:context=".CatalogueListActivity">
+
+    <item
+        android:id="@+id/action_search"
+        android:title="@string/action_search"
+        android:icon="@drawable/ic_search_white_24dp"
+        app:showAsAction="collapseActionView|ifRoom"
+        app:actionViewClass="android.support.v7.widget.SearchView"/>
+
+    <item android:id="@+id/action_settings"
+        android:title="@string/pref_category_sources"
+        android:icon="@drawable/ic_settings_white_24dp"
+        app:showAsAction="ifRoom"/>
+</menu>

+ 11 - 0
app/src/main/res/menu/catalogue_new_list.xml

@@ -0,0 +1,11 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools" tools:context=".CatalogueListActivity">
+
+    <item
+        android:id="@+id/action_search"
+        android:title="@string/action_search"
+        android:icon="@drawable/ic_search_white_24dp"
+        app:showAsAction="collapseActionView|ifRoom"
+        app:actionViewClass="android.support.v7.widget.SearchView"/>
+</menu>

+ 8 - 1
app/src/main/res/values/strings.xml

@@ -34,6 +34,7 @@
     <string name="action_sort_last_read">Last read</string>
     <string name="action_sort_last_updated">Last updated</string>
     <string name="action_search">Search</string>
+    <string name="action_global_search">Global search</string>
     <string name="action_select_all">Select all</string>
     <string name="action_mark_as_read">Mark as read</string>
     <string name="action_mark_as_unread">Mark as unread</string>
@@ -85,6 +86,8 @@
     <string name="action_open_log">Open log</string>
     <string name="action_create">Create</string>
     <string name="action_restore">Restore</string>
+    <string name="action_open">Open</string>
+    <string name="action_login">Login</string>
 
     <!-- Operations -->
     <string name="deleting">Deleting…</string>
@@ -276,8 +279,13 @@
     <string name="no_valid_sources">Please enable at least one valid source</string>
     <string name="no_more_results">No more results</string>
     <string name="local_source">Local manga</string>
+    <string name="other_source">Other</string>
     <string name="invalid_combination">Default can\'t be selected with other categories</string>
     <string name="added_to_library">The manga has been added to your library</string>
+    <string name="action_global_search_hint">Global search…</string>
+    <string name="no_results">No results found!</string>
+    <string name="latest">Latest</string>
+    <string name="browse">Browse</string>
 
     <!-- Manga activity -->
     <string name="manga_not_in_db">This manga was removed from the database!</string>
@@ -430,5 +438,4 @@
     <string name="download_notifier_text_only_wifi">No wifi connection available</string>
     <string name="download_notifier_no_network">No network connection available</string>
     <string name="download_notifier_download_paused">Download paused</string>
-
 </resources>

+ 32 - 17
app/src/main/res/values/styles.xml

@@ -4,7 +4,7 @@
     <!--========-->
     <!--Toolbars-->
     <!--========-->
-    <style name="Theme.ActionBar" parent="@style/ThemeOverlay.AppCompat.ActionBar"/>
+    <style name="Theme.ActionBar" parent="@style/ThemeOverlay.AppCompat.ActionBar" />
 
     <style name="Theme.ActionBar.Light" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
         <item name="popupTheme">@style/ThemeOverlay.AppCompat.Light</item>
@@ -13,12 +13,12 @@
     <!--====-->
     <!--Tabs-->
     <!--====-->
-    <style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
+    <style name="Theme.ActionBar.Tab" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
 
     <!--===========-->
     <!--AlertDialog-->
     <!--===========-->
-    <style name="Theme.AlertDialog"/>
+    <style name="Theme.AlertDialog" />
 
     <style name="Theme.AlertDialog.Light" parent="Theme.AppCompat.Light.Dialog.Alert">
         <item name="android:windowMinWidthMajor">@android:dimen/dialog_min_width_major</item>
@@ -35,7 +35,7 @@
     <!--==============-->
     <!--NavigationView-->
     <!--==============-->
-    <style name="Theme.Widget.NavigationView"/>
+    <style name="Theme.Widget.NavigationView" />
 
     <style name="Theme.Widget.NavigationView.Dark">
         <item name="colorControlHighlight">@color/md_grey_900</item>
@@ -85,6 +85,10 @@
         <item name="android:textSize">16sp</item>
     </style>
 
+    <style name="TextAppearance.Regular.SubHeading.Upper">
+        <item name="android:textAllCaps">true</item>
+    </style>
+
     <style name="TextAppearance.Regular.SubHeading.Secondary">
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
@@ -105,6 +109,10 @@
         <item name="android:textSize">20sp</item>
     </style>
 
+    <style name="TextAppearance.Medium.Title.Upper">
+        <item name="android:textAllCaps">true</item>
+    </style>
+
     <style name="TextAppearance.Medium.Title.Secondary">
         <item name="android:textColor">?android:attr/textColorSecondary</item>
     </style>
@@ -130,7 +138,7 @@
     <!--=======-->
     <!--Widgets-->
     <!--=======-->
-    <style name="Theme.Widget"/>
+    <style name="Theme.Widget" />
 
     <style name="Theme.Widget.FAB">
         <item name="android:layout_height">@dimen/fab_size</item>
@@ -147,10 +155,16 @@
         <item name="android:layout_width">match_parent</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:padding">@dimen/material_component_cards_top_and_bottom_padding</item>
-        <item name="android:layout_marginTop">@dimen/material_component_cards_space_between_cards</item>
-        <item name="android:layout_marginBottom">@dimen/material_component_cards_space_between_cards</item>
-        <item name="android:layout_marginStart">@dimen/material_component_cards_space_between_cards</item>
-        <item name="android:layout_marginEnd">@dimen/material_component_cards_space_between_cards</item>
+        <item name="android:layout_marginTop">@dimen/material_component_cards_space_between_cards
+        </item>
+        <item name="android:layout_marginBottom">
+            @dimen/material_component_cards_space_between_cards
+        </item>
+        <item name="android:layout_marginStart">
+            @dimen/material_component_cards_space_between_cards
+        </item>
+        <item name="android:layout_marginEnd">@dimen/material_component_cards_space_between_cards
+        </item>
         <item name="cardBackgroundColor">?attr/background_card</item>
         <item name="cardElevation">2dp</item>
     </style>
@@ -161,21 +175,24 @@
     </style>
 
     <style name="Theme.Widget.GridView">
+        <item name="android:smoothScrollbar">true</item>
+        <item name="android:numColumns">auto_fit</item>
+        <item name="android:stretchMode">columnWidth</item>
+        <item name="android:scrollbarStyle">outsideOverlay</item>
+    </style>
+
+    <style name="Theme.Widget.GridView.Catalogue">
         <item name="android:padding">5dp</item>
-        <item name="android:clipToPadding">false</item>
         <item name="android:gravity">top|left</item>
         <item name="android:smoothScrollbar">true</item>
         <item name="android:cacheColorHint">?android:attr/textColorHint</item>
         <item name="android:fastScrollEnabled">true</item>
         <item name="android:horizontalSpacing">0dp</item>
         <item name="android:verticalSpacing">0dp</item>
-        <item name="android:numColumns">auto_fit</item>
-        <item name="android:stretchMode">columnWidth</item>
-        <item name="android:scrollbarStyle">outsideOverlay</item>
     </style>
 
 
-    <style name="Theme.Widget.CheckBox"/>
+    <style name="Theme.Widget.CheckBox" />
 
     <style name="Theme.Widget.CheckBox.Light" parent="TextAppearance.Regular.Body1.Light">
         <item name="buttonTint">@color/md_white_1000</item>
@@ -212,8 +229,7 @@
         <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
     </style>
 
-    <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert">
-    </style>
+    <style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Light.Dialog.Alert"></style>
 
     <style name="reader_settings_popup_animation">
         <item name="android:windowEnterAnimation">@anim/enter_from_right</item>
@@ -226,5 +242,4 @@
     </style>
 
 
-
 </resources>