diff --git a/app/build.gradle b/app/build.gradle
index 7c5577ad7..9564168e1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -191,6 +191,7 @@ dependencies {
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
+ compile 'com.github.mthli:Slice:v1.2'
// Conductor
compile "com.bluelinelabs:conductor:2.1.4"
@@ -275,4 +276,4 @@ afterEvaluate {
}
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
index 9f55cd033..07649e2fd 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/BaseController.kt
@@ -34,7 +34,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
return null
}
- private fun setTitle() {
+ fun setTitle() {
var parentController = parentController
while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
index 63eba25ed..3f252409c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/NucleusController.kt
@@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter
@Suppress("LeakingThis")
-abstract class NucleusController
>(val bundle: Bundle? = null) : RxController(),
+abstract class NucleusController
>(val bundle: Bundle? = null) : RxController(bundle),
PresenterFactory
{
private val delegate = NucleusConductorDelegate(this)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
index a98ee3e2e..dc53757dd 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt
@@ -4,24 +4,20 @@ import android.content.res.Configuration
import android.os.Bundle
import android.support.design.widget.Snackbar
import android.support.v4.widget.DrawerLayout
-import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.*
import android.view.*
-import android.widget.AdapterView
-import android.widget.ArrayAdapter
-import android.widget.Spinner
import com.afollestad.materialdialogs.MaterialDialog
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
import com.f2prateek.rx.preferences.Preference
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
-import com.jakewharton.rxbinding.widget.itemSelections
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
@@ -43,7 +39,7 @@ import java.util.concurrent.TimeUnit
/**
* Controller to manage the catalogues available in the app.
*/
-open class CatalogueController(bundle: Bundle? = null) :
+open class CatalogueController(bundle: Bundle) :
NucleusController(bundle),
SecondaryDrawerController,
FlexibleAdapter.OnItemClickListener,
@@ -51,6 +47,10 @@ open class CatalogueController(bundle: Bundle? = null) :
FlexibleAdapter.EndlessScrollListener,
ChangeMangaCategoriesDialog.Listener {
+ constructor(source: CatalogueSource) : this(Bundle().apply {
+ putLong(SOURCE_ID_KEY, source.id)
+ })
+
/**
* Preferences helper.
*/
@@ -61,11 +61,6 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private var adapter: FlexibleAdapter>? = null
- /**
- * Spinner shown in the toolbar to change the selected source.
- */
- private var spinner: Spinner? = null
-
/**
* Snackbar containing an error message when a request fails.
*/
@@ -81,26 +76,24 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private var recycler: RecyclerView? = null
+ /**
+ * Drawer listener to allow swipe only for closing the drawer.
+ */
private var drawerListener: DrawerLayout.DrawerListener? = null
- /**
- * Query of the search box.
- */
- private val query: String
- get() = presenter.query
-
- /**
- * Selected index of the spinner (selected source).
- */
- private var selectedIndex: Int = 0
-
/**
* Subscription for the search view.
*/
private var searchViewSubscription: Subscription? = null
+ /**
+ * Subscription for the number of manga per row.
+ */
private var numColumnsSubscription: Subscription? = null
+ /**
+ * Endless loading item.
+ */
private var progressItem: ProgressItem? = null
init {
@@ -108,11 +101,11 @@ open class CatalogueController(bundle: Bundle? = null) :
}
override fun getTitle(): String? {
- return ""
+ return presenter.source.toString()
}
override fun createPresenter(): CataloguePresenter {
- return CataloguePresenter()
+ return CataloguePresenter(args.getLong(SOURCE_ID_KEY))
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@@ -126,54 +119,18 @@ open class CatalogueController(bundle: Bundle? = null) :
adapter = FlexibleAdapter(null, this)
setupRecycler(view)
- // Create toolbar spinner
- val themedContext = (activity as AppCompatActivity).supportActionBar?.themedContext
- ?: activity
-
- val spinnerAdapter = ArrayAdapter(themedContext,
- android.R.layout.simple_spinner_item, presenter.sources)
- spinnerAdapter.setDropDownViewResource(R.layout.common_spinner_item)
-
- val onItemSelected: (Int) -> Unit = { position ->
- val source = spinnerAdapter.getItem(position)
- if (!presenter.isValidSource(source)) {
- spinner?.setSelection(selectedIndex)
- activity?.toast(R.string.source_requires_login)
- } else if (source != presenter.source) {
- selectedIndex = position
- showProgressBar()
- adapter?.clear()
- presenter.setActiveSource(source)
- navView?.setFilters(presenter.filterItems)
- activity?.invalidateOptionsMenu()
- }
- }
-
- selectedIndex = presenter.sources.indexOf(presenter.source)
-
- spinner = Spinner(themedContext).apply {
- adapter = spinnerAdapter
- setSelection(selectedIndex)
- itemSelections()
- .skip(1)
- .filter { it != AdapterView.INVALID_POSITION }
- .subscribeUntilDestroy { onItemSelected(it) }
- }
-
- activity?.toolbar?.addView(spinner)
+ navView?.setFilters(presenter.filterItems)
view.progress?.visible()
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
- activity?.toolbar?.removeView(spinner)
numColumnsSubscription?.unsubscribe()
numColumnsSubscription = null
searchViewSubscription?.unsubscribe()
searchViewSubscription = null
adapter = null
- spinner = null
snack = null
recycler = null
}
@@ -265,6 +222,7 @@ open class CatalogueController(bundle: Bundle? = null) :
menu.findItem(R.id.action_search).apply {
val searchView = actionView as SearchView
+ val query = presenter.query
if (!query.isBlank()) {
expandActionView()
searchView.setQuery(query, true)
@@ -328,7 +286,7 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
private fun searchWithQuery(newQuery: String) {
// If text didn't change, do nothing
- if (query == newQuery)
+ if (presenter.query == newQuery)
return
// FIXME dirty fix to restore the toolbar buttons after closing search mode.
@@ -447,9 +405,9 @@ open class CatalogueController(bundle: Bundle? = null) :
*/
fun getColumnsPreferenceForCurrentOrientation(): Preference {
return if (resources?.configuration?.orientation == Configuration.ORIENTATION_PORTRAIT)
- presenter.prefs.portraitColumns()
+ preferences.portraitColumns()
else
- presenter.prefs.landscapeColumns()
+ preferences.landscapeColumns()
}
/**
@@ -558,4 +516,8 @@ open class CatalogueController(bundle: Bundle? = null) :
presenter.updateMangaCategories(manga, categories)
}
+ protected companion object {
+ const val SOURCE_ID_KEY = "sourceId"
+ }
+
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
index 69fd34db7..82e1b4d56 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt
@@ -9,15 +9,11 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
-import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.CatalogueSource
-import eu.kanade.tachiyomi.source.LocalSource
-import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.SManga
-import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.catalogue.filter.*
import rx.Observable
@@ -33,22 +29,17 @@ import uy.kohesive.injekt.api.get
* Presenter of [CatalogueController].
*/
open class CataloguePresenter(
- val sourceManager: SourceManager = Injekt.get(),
- val db: DatabaseHelper = Injekt.get(),
- val prefs: PreferencesHelper = Injekt.get(),
- val coverCache: CoverCache = Injekt.get()
+ sourceId: Long,
+ sourceManager: SourceManager = Injekt.get(),
+ private val db: DatabaseHelper = Injekt.get(),
+ private val prefs: PreferencesHelper = Injekt.get(),
+ private val coverCache: CoverCache = Injekt.get()
) : BasePresenter() {
/**
- * Enabled sources.
+ * Selected source.
*/
- val sources by lazy { getEnabledSources() }
-
- /**
- * Active source.
- */
- lateinit var source: CatalogueSource
- private set
+ val source = sourceManager.get(sourceId) as CatalogueSource
/**
* Query from the view.
@@ -106,7 +97,6 @@ open class CataloguePresenter(
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
- source = getLastUsedSource()
sourceFilters = source.getFilterList()
if (savedState != null) {
@@ -149,9 +139,9 @@ open class CataloguePresenter(
.doOnNext { initializeMangas(it.second) }
.map { it.first to it.second.map(::CatalogueItem) }
.observeOn(AndroidSchedulers.mainThread())
- .subscribeReplay({ view, pair ->
- view.onAddPage(pair.first, pair.second)
- }, { view, error ->
+ .subscribeReplay({ view, (page, mangas) ->
+ view.onAddPage(page, mangas)
+ }, { _, error ->
Timber.e(error)
})
@@ -167,7 +157,7 @@ open class CataloguePresenter(
pageSubscription?.let { remove(it) }
pageSubscription = Observable.defer { pager.requestNext() }
- .subscribeFirst({ view, page ->
+ .subscribeFirst({ _, _ ->
// Nothing to do when onNext is emitted.
}, CatalogueController::onAddPageError)
}
@@ -179,19 +169,6 @@ open class CataloguePresenter(
return pager.hasNextPage
}
- /**
- * Sets the active source and restarts the pager.
- *
- * @param source the new active source.
- */
- fun setActiveSource(source: CatalogueSource) {
- prefs.lastUsedCatalogueSource().set(source.id)
- this.source = source
- sourceFilters = source.getFilterList()
-
- restartPager(query = "", filters = FilterList())
- }
-
/**
* Sets the display mode.
*
@@ -267,50 +244,6 @@ open class CataloguePresenter(
.onErrorResumeNext { Observable.just(manga) }
}
- /**
- * Returns the last used source from preferences or the first valid source.
- *
- * @return a source.
- */
- fun getLastUsedSource(): CatalogueSource {
- val id = prefs.lastUsedCatalogueSource().get() ?: -1
- val source = sourceManager.get(id)
- if (!isValidSource(source) || source !in sources) {
- return sources.first { isValidSource(it) }
- }
- return source as CatalogueSource
- }
-
- /**
- * Checks if the given source is valid.
- *
- * @param source the source to check.
- * @return true if the source is valid, false otherwise.
- */
- open fun isValidSource(source: Source?): Boolean {
- if (source == null) return false
-
- if (source is LoginSource) {
- return source.isLogged() ||
- (prefs.sourceUsername(source) != "" && prefs.sourcePassword(source) != "")
- }
- return true
- }
-
- /**
- * Returns a list of enabled sources ordered by language and name.
- */
- open protected fun getEnabledSources(): List {
- val languages = prefs.enabledLanguages().getOrDefault()
- val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
-
- return sourceManager.getCatalogueSources()
- .filter { it.lang in languages }
- .filterNot { it.id.toString() in hiddenCatalogues }
- .sortedBy { "(${it.lang}) ${it.name}" } +
- sourceManager.get(LocalSource.ID) as LocalSource
- }
-
/**
* Adds or removes a manga from the library.
*
@@ -370,13 +303,12 @@ open class CataloguePresenter(
}
is Filter.Sort -> {
val group = SortGroup(it)
- val subItems = it.values.mapNotNull {
+ val subItems = it.values.map {
SortItem(it, group)
}
group.subItems = subItems
group
}
- else -> null
}
}
}
@@ -407,7 +339,7 @@ open class CataloguePresenter(
* @param categories the selected categories.
* @param manga the manga to move.
*/
- fun moveMangaToCategories(manga: Manga, categories: List) {
+ private fun moveMangaToCategories(manga: Manga, categories: List) {
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
db.setMangaCategories(mc, listOf(manga))
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt
new file mode 100644
index 000000000..0b1b822e0
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchAdapter.kt
@@ -0,0 +1,74 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.os.Bundle
+import android.os.Parcelable
+import android.support.v7.widget.RecyclerView
+import android.util.SparseArray
+import eu.davidea.flexibleadapter.FlexibleAdapter
+
+/**
+ * Adapter that holds the search cards.
+ *
+ * @param controller instance of [CatalogueSearchController].
+ */
+class CatalogueSearchAdapter(val controller: CatalogueSearchController) :
+ FlexibleAdapter(null, controller, true) {
+
+ /**
+ * Bundle where the view state of the holders is saved.
+ */
+ private var bundle = Bundle()
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: List?) {
+ super.onBindViewHolder(holder, position, payloads)
+ restoreHolderState(holder)
+ }
+
+ override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
+ super.onViewRecycled(holder)
+ saveHolderState(holder, bundle)
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ val holdersBundle = Bundle()
+ allBoundViewHolders.forEach { saveHolderState(it, holdersBundle) }
+ outState.putBundle(HOLDER_BUNDLE_KEY, holdersBundle)
+ super.onSaveInstanceState(outState)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ bundle = savedInstanceState.getBundle(HOLDER_BUNDLE_KEY)
+ }
+
+ /**
+ * Saves the view state of the given holder.
+ *
+ * @param holder The holder to save.
+ * @param outState The bundle where the state is saved.
+ */
+ private fun saveHolderState(holder: RecyclerView.ViewHolder, outState: Bundle) {
+ val key = "holder_${holder.adapterPosition}"
+ val holderState = SparseArray()
+ holder.itemView.saveHierarchyState(holderState)
+ outState.putSparseParcelableArray(key, holderState)
+ }
+
+ /**
+ * Restores the view state of the given holder.
+ *
+ * @param holder The holder to restore.
+ */
+ private fun restoreHolderState(holder: RecyclerView.ViewHolder) {
+ val key = "holder_${holder.adapterPosition}"
+ val holderState = bundle.getSparseParcelableArray(key)
+ if (holderState != null) {
+ holder.itemView.restoreHierarchyState(holderState)
+ bundle.remove(key)
+ }
+ }
+
+ private companion object {
+ const val HOLDER_BUNDLE_KEY = "holder_bundle"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt
new file mode 100644
index 000000000..17791f3be
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardAdapter.kt
@@ -0,0 +1,27 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+
+/**
+ * Adapter that holds the manga items from search results.
+ *
+ * @param controller instance of [CatalogueSearchController].
+ */
+class CatalogueSearchCardAdapter(controller: CatalogueSearchController) :
+ FlexibleAdapter(null, controller, true) {
+
+ /**
+ * Listen for browse item clicks.
+ */
+ val mangaClickListener: OnMangaClickListener = controller
+
+ /**
+ * Listener which should be called when user clicks browse.
+ * Note: Should only be handled by [CatalogueSearchController]
+ */
+ interface OnMangaClickListener {
+ fun onMangaClick(manga: Manga)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt
new file mode 100644
index 000000000..02c102b8f
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardHolder.kt
@@ -0,0 +1,43 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.View
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.widget.StateImageViewTarget
+import kotlinx.android.synthetic.main.catalogue_global_search_controller_card_item.view.*
+
+class CatalogueSearchCardHolder(view: View, adapter: CatalogueSearchCardAdapter)
+ : FlexibleViewHolder(view, adapter) {
+
+ init {
+ // Call onMangaClickListener when item is pressed.
+ itemView.setOnClickListener {
+ val item = adapter.getItem(adapterPosition)
+ if (item != null) {
+ adapter.mangaClickListener.onMangaClick(item.manga)
+ }
+ }
+ }
+
+ fun bind(manga: Manga) {
+ itemView.tvTitle.text = manga.title
+
+ setImage(manga)
+ }
+
+ fun setImage(manga: Manga) {
+ Glide.clear(itemView.itemImage)
+ if (!manga.thumbnail_url.isNullOrEmpty()) {
+ Glide.with(itemView.context)
+ .load(manga)
+ .diskCacheStrategy(DiskCacheStrategy.SOURCE)
+ .centerCrop()
+ .skipMemoryCache(true)
+ .placeholder(android.R.color.transparent)
+ .into(StateImageViewTarget(itemView.itemImage, itemView.progress))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt
new file mode 100644
index 000000000..b721df342
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchCardItem.kt
@@ -0,0 +1,38 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.util.inflate
+
+class CatalogueSearchCardItem(val manga: Manga) : AbstractFlexibleItem() {
+
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_global_search_controller_card_item
+ }
+
+ override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
+ parent: ViewGroup): CatalogueSearchCardHolder {
+ return CatalogueSearchCardHolder(parent.inflate(layoutRes), adapter as CatalogueSearchCardAdapter)
+ }
+
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchCardHolder,
+ position: Int, payloads: List?) {
+ holder.bind(manga)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is CatalogueSearchCardItem) {
+ return manga.id == other.manga.id
+ }
+ return false
+ }
+
+ override fun hashCode(): Int {
+ return manga.id?.toInt() ?: 0
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt
new file mode 100644
index 000000000..ea10a038e
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt
@@ -0,0 +1,171 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.SearchView
+import android.view.*
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.manga.MangaController
+import kotlinx.android.synthetic.main.catalogue_global_search_controller.view.*
+
+/**
+ * This controller shows and manages the different search result in global search.
+ * This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter]
+ * [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search
+ */
+class CatalogueSearchController(private val initialQuery: String? = null) :
+ NucleusController(),
+ CatalogueSearchCardAdapter.OnMangaClickListener {
+
+ /**
+ * Adapter containing search results grouped by lang.
+ */
+ private var adapter: CatalogueSearchAdapter? = null
+
+ /**
+ * Called when controller is initialized.
+ */
+ init {
+ setHasOptionsMenu(true)
+ }
+
+ /**
+ * Initiate the view with [R.layout.catalogue_global_search_controller].
+ *
+ * @param inflater used to load the layout xml.
+ * @param container containing parent views.
+ * @return inflated view
+ */
+ override fun inflateView(inflater: LayoutInflater, container: ViewGroup): android.view.View {
+ return inflater.inflate(R.layout.catalogue_global_search_controller, container, false)
+ }
+
+ /**
+ * Set the title of controller.
+ *
+ * @return title.
+ */
+ override fun getTitle(): String? {
+ return presenter.query
+ }
+
+ /**
+ * Create the [CatalogueSearchPresenter] used in controller.
+ *
+ * @return instance of [CatalogueSearchPresenter]
+ */
+ override fun createPresenter(): CatalogueSearchPresenter {
+ return CatalogueSearchPresenter(initialQuery)
+ }
+
+ /**
+ * Called when manga in global search is clicked, opens manga.
+ *
+ * @param manga clicked item containing manga information.
+ */
+ override fun onMangaClick(manga: Manga) {
+ // Open MangaController.
+ router.pushController(RouterTransaction.with(MangaController(manga, true))
+ .pushChangeHandler(FadeChangeHandler())
+ .popChangeHandler(FadeChangeHandler()))
+ }
+
+ /**
+ * Adds items to the options menu.
+ *
+ * @param menu menu containing options.
+ * @param inflater used to load the menu xml.
+ */
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ // Inflate menu.
+ inflater.inflate(R.menu.catalogue_new_list, menu)
+
+ // Initialize search menu
+ val searchItem = menu.findItem(R.id.action_search)
+ val searchView = searchItem.actionView as SearchView
+ searchView.queryTextChangeEvents()
+ .filter { it.isSubmitted }
+ .subscribeUntilDestroy {
+ presenter.search(it.queryText().toString())
+ searchItem.collapseActionView()
+ setTitle() // Update toolbar title
+ }
+ }
+
+ /**
+ * Called when the view is created
+ *
+ * @param view view of controller
+ * @param savedViewState information from previous state.
+ */
+ override fun onViewCreated(view: View, savedViewState: Bundle?) {
+ super.onViewCreated(view, savedViewState)
+
+ adapter = CatalogueSearchAdapter(this)
+
+ with(view) {
+ // Create recycler and set adapter.
+ recycler.layoutManager = LinearLayoutManager(context)
+ recycler.adapter = adapter
+ }
+ }
+
+ override fun onDestroyView(view: View) {
+ adapter = null
+ super.onDestroyView(view)
+ }
+
+ override fun onSaveViewState(view: View, outState: Bundle) {
+ super.onSaveViewState(view, outState)
+ adapter?.onSaveInstanceState(outState)
+ }
+
+ override fun onRestoreViewState(view: View, savedViewState: Bundle) {
+ super.onRestoreViewState(view, savedViewState)
+ adapter?.onRestoreInstanceState(savedViewState)
+ }
+
+ /**
+ * Returns the view holder for the given manga.
+ *
+ * @param source used to find holder containing source
+ * @return the holder of the manga or null if it's not bound.
+ */
+ private fun getHolder(source: CatalogueSource): CatalogueSearchHolder? {
+ val adapter = adapter ?: return null
+
+ adapter.allBoundViewHolders.forEach { holder ->
+ val item = adapter.getItem(holder.adapterPosition)
+ if (item != null && source.id == item.source.id) {
+ return holder as CatalogueSearchHolder
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Add search result to adapter.
+ *
+ * @param searchResult result of search.
+ */
+ fun setItems(searchResult: List) {
+ adapter?.updateDataSet(searchResult)
+ }
+
+ /**
+ * Called from the presenter when a manga is initialized.
+ *
+ * @param manga the initialized manga.
+ */
+ fun onMangaInitialized(source: CatalogueSource, manga: Manga) {
+ getHolder(source)?.setImage(manga)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt
new file mode 100644
index 000000000..0714c4342
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchHolder.kt
@@ -0,0 +1,100 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.support.v7.widget.LinearLayoutManager
+import android.view.View
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.util.getResourceColor
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.setVectorCompat
+import eu.kanade.tachiyomi.util.visible
+import kotlinx.android.synthetic.main.catalogue_global_search_controller_card.view.*
+
+/**
+ * Holder that binds the [CatalogueSearchItem] containing catalogue cards.
+ *
+ * @param view view of [CatalogueSearchItem]
+ * @param adapter instance of [CatalogueSearchAdapter]
+ */
+class CatalogueSearchHolder(view: View, val adapter: CatalogueSearchAdapter) : FlexibleViewHolder(view, adapter) {
+
+ /**
+ * Adapter containing manga from search results.
+ */
+ private val mangaAdapter = CatalogueSearchCardAdapter(adapter.controller)
+
+ private var lastBoundResults: List? = null
+
+ init {
+ with(itemView) {
+ // Set layout horizontal.
+ recycler.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ recycler.adapter = mangaAdapter
+
+ nothing_found_icon.setVectorCompat(R.drawable.ic_search_black_112dp,
+ context.getResourceColor(android.R.attr.textColorHint))
+ }
+ }
+
+ /**
+ * Show the loading of source search result.
+ *
+ * @param item item of card.
+ */
+ fun bind(item: CatalogueSearchItem) {
+ val source = item.source
+ val results = item.results
+
+ with(itemView) {
+ // Set Title witch country code if available.
+ title.text = if (!source.lang.isEmpty()) "${source.name} (${source.lang})" else source.name
+
+ when {
+ results == null -> {
+ progress.visible()
+ nothing_found.gone()
+ }
+ results.isEmpty() -> {
+ progress.gone()
+ nothing_found.visible()
+ }
+ else -> {
+ progress.gone()
+ nothing_found.gone()
+ }
+ }
+ if (results !== lastBoundResults) {
+ mangaAdapter.updateDataSet(results)
+ lastBoundResults = results
+ }
+ }
+ }
+
+ /**
+ * Called from the presenter when a manga is initialized.
+ *
+ * @param manga the initialized manga.
+ */
+ fun setImage(manga: Manga) {
+ getHolder(manga)?.setImage(manga)
+ }
+
+ /**
+ * Returns the view holder for the given manga.
+ *
+ * @param manga the manga to find.
+ * @return the holder of the manga or null if it's not bound.
+ */
+ private fun getHolder(manga: Manga): CatalogueSearchCardHolder? {
+ mangaAdapter.allBoundViewHolders.forEach { holder ->
+ val item = mangaAdapter.getItem(holder.adapterPosition)
+ if (item != null && item.manga.id!! == manga.id!!) {
+ return holder as CatalogueSearchCardHolder
+ }
+ }
+
+ return null
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt
new file mode 100644
index 000000000..265d67349
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchItem.kt
@@ -0,0 +1,67 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.util.inflate
+
+/**
+ * Item that contains search result information.
+ *
+ * @param source contains information about search result.
+ */
+class CatalogueSearchItem(val source: CatalogueSource, val results: List?)
+ : AbstractFlexibleItem() {
+
+ /**
+ * Set view.
+ *
+ * @return id of view
+ */
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_global_search_controller_card
+ }
+
+ /**
+ * Create view holder (see [CatalogueSearchAdapter].
+ *
+ * @return holder of view.
+ */
+ override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
+ parent: ViewGroup): CatalogueSearchHolder {
+ return CatalogueSearchHolder(parent.inflate(layoutRes), adapter as CatalogueSearchAdapter)
+ }
+
+ /**
+ * Bind item to view.
+ */
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: CatalogueSearchHolder,
+ position: Int, payloads: List?) {
+ holder.bind(this)
+ }
+
+ /**
+ * Used to check if two items are equal.
+ *
+ * @return items are equal?
+ */
+ override fun equals(other: Any?): Boolean {
+ if (other is CatalogueSearchItem) {
+ return source.id == other.source.id
+ }
+ return false
+ }
+
+ /**
+ * Return hash code of item.
+ *
+ * @return hashcode
+ */
+ override fun hashCode(): Int {
+ return source.id.toInt()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt
new file mode 100644
index 000000000..243b79430
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt
@@ -0,0 +1,215 @@
+package eu.kanade.tachiyomi.ui.catalogue.global_search
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.Source
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.source.model.SManga
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import rx.subjects.PublishSubject
+import timber.log.Timber
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+/**
+ * Presenter of [CatalogueSearchController]
+ * Function calls should be done from here. UI calls should be done from the controller.
+ *
+ * @param sourceManager manages the different sources.
+ * @param db manages the database calls.
+ * @param preferencesHelper manages the preference calls.
+ */
+class CatalogueSearchPresenter(
+ val initialQuery: String? = "",
+ val sourceManager: SourceManager = Injekt.get(),
+ val db: DatabaseHelper = Injekt.get(),
+ val preferencesHelper: PreferencesHelper = Injekt.get()
+) : BasePresenter() {
+
+ /**
+ * Enabled sources.
+ */
+ val sources by lazy { getEnabledSources() }
+
+ /**
+ * Query from the view.
+ */
+ var query = ""
+ private set
+
+ /**
+ * Fetches the different sources by user settings.
+ */
+ private var fetchSourcesSubscription: Subscription? = null
+
+ /**
+ * Subject which fetches image of given manga.
+ */
+ private val fetchImageSubject = PublishSubject.create, Source>>()
+
+ /**
+ * Subscription for fetching images of manga.
+ */
+ private var fetchImageSubscription: Subscription? = null
+
+ override fun onCreate(savedState: Bundle?) {
+ super.onCreate(savedState)
+
+ // Perform a search with previous or initial state
+ search(savedState?.getString(CataloguePresenter::query.name) ?: initialQuery.orEmpty())
+ }
+
+ override fun onDestroy() {
+ fetchSourcesSubscription?.unsubscribe()
+ fetchImageSubscription?.unsubscribe()
+ super.onDestroy()
+ }
+
+ override fun onSave(state: Bundle) {
+ state.putString(CataloguePresenter::query.name, query)
+ super.onSave(state)
+ }
+
+ /**
+ * Returns a list of enabled sources ordered by language and name.
+ *
+ * @return list containing enabled sources.
+ */
+ private fun getEnabledSources(): List {
+ val languages = preferencesHelper.enabledLanguages().getOrDefault()
+ val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault()
+
+ return sourceManager.getCatalogueSources()
+ .filter { it.lang in languages }
+ .filterNot { it is LoginSource && !it.isLogged() }
+ .filterNot { it.id.toString() in hiddenCatalogues }
+ .sortedBy { "(${it.lang}) ${it.name}" }
+ }
+
+ /**
+ * Initiates a search for mnaga per catalogue.
+ *
+ * @param query query on which to search.
+ */
+ fun search(query: String) {
+ // Return if there's nothing to do
+ if (this.query == query) return
+
+ // Update query
+ this.query = query
+
+ // Create image fetch subscription
+ initializeFetchImageSubscription()
+
+ // Create items with the initial state
+ val initialItems = sources.map { CatalogueSearchItem(it, null) }
+ var items = initialItems
+
+ fetchSourcesSubscription?.unsubscribe()
+ fetchSourcesSubscription = Observable.from(sources)
+ .observeOn(Schedulers.io())
+ .flatMap { source ->
+ source.fetchSearchManga(1, query, FilterList())
+ .onExceptionResumeNext(Observable.empty()) // Ignore timeouts.
+ .map { it.mangas.take(10) } // Get at most 10 manga from search result.
+ .map { it.map { networkToLocalManga(it, source.id) } } // Convert to local manga.
+ .doOnNext { fetchImage(it, source) } // Load manga covers.
+ .map { CatalogueSearchItem(source, it.map { CatalogueSearchCardItem(it) }) }
+ }
+ .observeOn(AndroidSchedulers.mainThread())
+ // Update matching source with the obtained results
+ .map { result ->
+ items.map { item -> if (item.source == result.source) result else item }
+ }
+ // Update current state
+ .doOnNext { items = it }
+ // Deliver initial state
+ .startWith(initialItems)
+ .subscribeLatestCache({ view, manga ->
+ view.setItems(manga)
+ }, { _, error ->
+ Timber.e(error)
+ })
+ }
+
+ /**
+ * Initialize a list of manga.
+ *
+ * @param manga the list of manga to initialize.
+ */
+ private fun fetchImage(manga: List, source: Source) {
+ fetchImageSubject.onNext(Pair(manga, source))
+ }
+
+ /**
+ * Subscribes to the initializer of manga details and updates the view if needed.
+ */
+ private fun initializeFetchImageSubscription() {
+ fetchImageSubscription?.unsubscribe()
+ fetchImageSubscription = fetchImageSubject.observeOn(Schedulers.io())
+ .flatMap {
+ val source = it.second
+ Observable.from(it.first).filter { it.thumbnail_url == null && !it.initialized }
+ .map { Pair(it, source) }
+ .concatMap { getMangaDetailsObservable(it.first, it.second) }
+ .map { Pair(source as CatalogueSource, it) }
+
+ }
+
+ .onBackpressureBuffer()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe({ (source, manga) ->
+ @Suppress("DEPRECATION")
+ view?.onMangaInitialized(source, manga)
+ }, { error ->
+ Timber.e(error)
+ })
+ }
+
+ /**
+ * Returns an observable of manga that initializes the given manga.
+ *
+ * @param manga the manga to initialize.
+ * @return an observable of the manga to initialize
+ */
+ private fun getMangaDetailsObservable(manga: Manga, source: Source): Observable {
+ return source.fetchMangaDetails(manga)
+ .flatMap { networkManga ->
+ manga.copyFrom(networkManga)
+ manga.initialized = true
+ db.insertManga(manga).executeAsBlocking()
+ Observable.just(manga)
+ }
+ .onErrorResumeNext { Observable.just(manga) }
+ }
+
+ /**
+ * Returns a manga from the database for the given manga from network. It creates a new entry
+ * if the manga is not yet in the database.
+ *
+ * @param sManga the manga from the source.
+ * @return a manga from the database.
+ */
+ private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
+ var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
+ if (localManga == null) {
+ val newManga = Manga.create(sManga.url, sManga.title, sourceId)
+ newManga.copyFrom(sManga)
+ val result = db.insertManga(newManga).executeAsBlocking()
+ newManga.id = result.insertedId()
+ localManga = newManga
+ }
+ return localManga
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt
new file mode 100644
index 000000000..d2e15169c
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainAdapter.kt
@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.getResourceColor
+
+/**
+ * Adapter that holds the catalogue cards.
+ *
+ * @param controller instance of [CatalogueMainController].
+ */
+class CatalogueMainAdapter(val controller: CatalogueMainController) :
+ FlexibleAdapter>(null, controller, true) {
+
+ val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
+
+ init {
+ setDisplayHeadersAtStartUp(true)
+ }
+
+ /**
+ * Listener for browse item clicks.
+ */
+ val browseClickListener: OnBrowseClickListener = controller
+
+ /**
+ * Listener for latest item clicks.
+ */
+ val latestClickListener: OnLatestClickListener = controller
+
+ /**
+ * Listener which should be called when user clicks browse.
+ * Note: Should only be handled by [CatalogueMainController]
+ */
+ interface OnBrowseClickListener {
+ fun onBrowseClick(position: Int)
+ }
+
+ /**
+ * Listener which should be called when user clicks latest.
+ * Note: Should only be handled by [CatalogueMainController]
+ */
+ interface OnLatestClickListener {
+ fun onLatestClick(position: Int)
+ }
+}
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt
new file mode 100644
index 000000000..205d6fb0d
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainController.kt
@@ -0,0 +1,238 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.os.Bundle
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.SearchView
+import android.view.*
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
+import com.bluelinelabs.conductor.RouterTransaction
+import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
+import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.IFlexible
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.ui.base.controller.NucleusController
+import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
+import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
+import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
+import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
+import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
+import kotlinx.android.synthetic.main.catalogue_main_controller.view.*
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+/**
+ * This controller shows and manages the different catalogues enabled by the user.
+ * This controller should only handle UI actions, IO actions should be done by [CatalogueMainPresenter]
+ * [SourceLoginDialog.Listener] refreshes the adapter on successful login of catalogues.
+ * [CatalogueMainAdapter.OnBrowseClickListener] call function data on browse item click.
+ * [CatalogueMainAdapter.OnLatestClickListener] call function data on latest item click
+ */
+class CatalogueMainController : NucleusController(),
+ SourceLoginDialog.Listener,
+ FlexibleAdapter.OnItemClickListener,
+ CatalogueMainAdapter.OnBrowseClickListener,
+ CatalogueMainAdapter.OnLatestClickListener {
+
+ /**
+ * Application preferences.
+ */
+ private val preferences: PreferencesHelper = Injekt.get()
+
+ /**
+ * Adapter containing sources.
+ */
+ private var adapter : CatalogueMainAdapter? = null
+
+ /**
+ * Called when controller is initialized.
+ */
+ init {
+ // Enable the option menu
+ setHasOptionsMenu(true)
+ }
+
+ /**
+ * Set the title of controller.
+ *
+ * @return title.
+ */
+ override fun getTitle(): String? {
+ return applicationContext?.getString(R.string.label_catalogues)
+ }
+
+ /**
+ * Create the [CatalogueMainPresenter] used in controller.
+ *
+ * @return instance of [CatalogueMainPresenter]
+ */
+ override fun createPresenter(): CatalogueMainPresenter {
+ return CatalogueMainPresenter()
+ }
+
+ /**
+ * Initiate the view with [R.layout.catalogue_main_controller].
+ *
+ * @param inflater used to load the layout xml.
+ * @param container containing parent views.
+ * @return inflated view.
+ */
+ override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
+ return inflater.inflate(R.layout.catalogue_main_controller, container, false)
+ }
+
+ /**
+ * Called when the view is created
+ *
+ * @param view view of controller
+ * @param savedViewState information from previous state.
+ */
+ override fun onViewCreated(view: View, savedViewState: Bundle?) {
+ super.onViewCreated(view, savedViewState)
+
+ adapter = CatalogueMainAdapter(this)
+
+ with(view) {
+ // Create recycler and set adapter.
+ recycler.layoutManager = LinearLayoutManager(context)
+ recycler.adapter = adapter
+ recycler.addItemDecoration(SourceDividerItemDecoration(context))
+ }
+ }
+
+ override fun onDestroyView(view: View) {
+ adapter = null
+ super.onDestroyView(view)
+ }
+
+ override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
+ super.onChangeStarted(handler, type)
+ if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
+ presenter.updateSources()
+ }
+ }
+
+ /**
+ * Called when login dialog is closed, refreshes the adapter.
+ *
+ * @param source clicked item containing source information.
+ */
+ override fun loginDialogClosed(source: LoginSource) {
+ if (source.isLogged()) {
+ adapter?.clear()
+ presenter.loadSources()
+ }
+ }
+
+ /**
+ * Called when item is clicked
+ */
+ override fun onItemClick(position: Int): Boolean {
+ val item = adapter?.getItem(position) as? SourceItem ?: return false
+ val source = item.source
+ if (source is LoginSource && !source.isLogged()) {
+ val dialog = SourceLoginDialog(source)
+ dialog.targetController = this
+ dialog.showDialog(router)
+ } else {
+ // Open the catalogue view.
+ openCatalogue(source, CatalogueController(source))
+ }
+ return false
+ }
+
+ /**
+ * Called when browse is clicked in [CatalogueMainAdapter]
+ */
+ override fun onBrowseClick(position: Int) {
+ onItemClick(position)
+ }
+
+ /**
+ * Called when latest is clicked in [CatalogueMainAdapter]
+ */
+ override fun onLatestClick(position: Int) {
+ val item = adapter?.getItem(position) as? SourceItem ?: return
+ openCatalogue(item.source, LatestUpdatesController(item.source))
+ }
+
+ /**
+ * Opens a catalogue with the given controller.
+ */
+ private fun openCatalogue(source: CatalogueSource, controller: CatalogueController) {
+ preferences.lastUsedCatalogueSource().set(source.id)
+ router.pushController(RouterTransaction.with(controller)
+ .popChangeHandler(FadeChangeHandler())
+ .pushChangeHandler(FadeChangeHandler()))
+ }
+
+ /**
+ * Adds items to the options menu.
+ *
+ * @param menu menu containing options.
+ * @param inflater used to load the menu xml.
+ */
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ // Inflate menu
+ inflater.inflate(R.menu.catalogue_main, menu)
+
+ // Initialize search option.
+ val searchItem = menu.findItem(R.id.action_search)
+ val searchView = searchItem.actionView as SearchView
+
+ // Change hint to show global search.
+ searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
+
+ // Create query listener which opens the global search view.
+ searchView.queryTextChangeEvents()
+ .filter { it.isSubmitted }
+ .subscribeUntilDestroy {
+ val query = it.queryText().toString()
+ router.pushController((RouterTransaction.with(CatalogueSearchController(query)))
+ .popChangeHandler(FadeChangeHandler())
+ .pushChangeHandler(FadeChangeHandler()))
+ }
+ }
+
+ /**
+ * Called when an option menu item has been selected by the user.
+ *
+ * @param item The selected item.
+ * @return True if this event has been consumed, false if it has not.
+ */
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ // Initialize option to open catalogue settings.
+ R.id.action_settings -> {
+ router.pushController((RouterTransaction.with(SettingsSourcesController()))
+ .popChangeHandler(SettingsSourcesFadeChangeHandler())
+ .pushChangeHandler(FadeChangeHandler()))
+ }
+ else -> return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ /**
+ * Called to update adapter containing sources.
+ */
+ fun setSources(sources: List>) {
+ adapter?.updateDataSet(sources.toMutableList())
+ }
+
+ /**
+ * Called to set the last used catalogue at the top of the view.
+ */
+ fun setLastUsedSource(item: SourceItem?) {
+ adapter?.removeAllScrollableHeaders()
+ if (item != null) {
+ adapter?.addScrollableHeader(item)
+ }
+ }
+
+ private class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt
new file mode 100644
index 000000000..2ce3f0a4f
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/CatalogueMainPresenter.kt
@@ -0,0 +1,97 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.source.LocalSource
+import eu.kanade.tachiyomi.source.SourceManager
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import rx.Observable
+import rx.Subscription
+import rx.android.schedulers.AndroidSchedulers
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+/**
+ * Presenter of [CatalogueMainController]
+ * Function calls should be done from here. UI calls should be done from the controller.
+ *
+ * @param sourceManager manages the different sources.
+ * @param preferences application preferences.
+ */
+class CatalogueMainPresenter(
+ val sourceManager: SourceManager = Injekt.get(),
+ private val preferences: PreferencesHelper = Injekt.get()
+) : BasePresenter() {
+
+ /**
+ * Enabled sources.
+ */
+ var sources = getEnabledSources()
+
+ /**
+ * Subscription for retrieving enabled sources.
+ */
+ private var sourceSubscription: Subscription? = null
+
+ override fun onCreate(savedState: Bundle?) {
+ super.onCreate(savedState)
+
+ // Load enabled and last used sources
+ loadSources()
+ loadLastUsedSource()
+ }
+
+ /**
+ * Unsubscribe and create a new subscription to fetch enabled sources.
+ */
+ fun loadSources() {
+ sourceSubscription?.unsubscribe()
+
+ val map = TreeMap> { d1, d2 -> d1.compareTo(d2) }
+ val byLang = sources.groupByTo(map, { it.lang })
+ val sourceItems = byLang.flatMap {
+ val langItem = LangItem(it.key)
+ it.value.map { source -> SourceItem(source, langItem) }
+ }
+
+ sourceSubscription = Observable.just(sourceItems)
+ .subscribeLatestCache(CatalogueMainController::setSources)
+ }
+
+ private fun loadLastUsedSource() {
+ val sharedObs = preferences.lastUsedCatalogueSource().asObservable().share()
+
+ // Emit the first item immediately but delay subsequent emissions by 500ms.
+ Observable.merge(
+ sharedObs.take(1),
+ sharedObs.skip(1).delay(500, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()))
+ .distinctUntilChanged()
+ .map { (sourceManager.get(it) as? CatalogueSource)?.let { SourceItem(it) } }
+ .subscribeLatestCache(CatalogueMainController::setLastUsedSource)
+ }
+
+ fun updateSources() {
+ sources = getEnabledSources()
+ loadSources()
+ }
+
+ /**
+ * Returns a list of enabled sources ordered by language and name.
+ *
+ * @return list containing enabled sources.
+ */
+ private fun getEnabledSources(): List {
+ val languages = preferences.enabledLanguages().getOrDefault()
+ val hiddenCatalogues = preferences.hiddenCatalogues().getOrDefault()
+
+ return sourceManager.getCatalogueSources()
+ .filter { it.lang in languages }
+ .filterNot { it.id.toString() in hiddenCatalogues }
+ .sortedBy { "(${it.lang}) ${it.name}" } +
+ sourceManager.get(LocalSource.ID) as LocalSource
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt
new file mode 100644
index 000000000..02dcea146
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangHolder.kt
@@ -0,0 +1,21 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.view.View
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import kotlinx.android.synthetic.main.catalogue_main_controller_card.view.*
+import java.util.*
+
+class LangHolder(view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter, true) {
+
+ fun bind(item: LangItem) {
+ itemView.title.text = when {
+ item.code == "" -> itemView.context.getString(R.string.other_source)
+ else -> {
+ val locale = Locale(item.code)
+ locale.getDisplayName(locale).capitalize()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt
new file mode 100644
index 000000000..55bdd5547
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/LangItem.kt
@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractHeaderItem
+import eu.kanade.tachiyomi.R
+
+/**
+ * Item that contains the language header.
+ *
+ * @param code The lang code.
+ */
+data class LangItem(val code: String) : AbstractHeaderItem() {
+
+ /**
+ * Returns the layout resource of this item.
+ */
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_main_controller_card
+ }
+
+ /**
+ * Creates a new view holder for this item.
+ */
+ override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
+ parent: ViewGroup): LangHolder {
+
+ return LangHolder(inflater.inflate(layoutRes, parent, false), adapter)
+ }
+
+ /**
+ * Binds this item to the given view holder.
+ */
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: LangHolder,
+ position: Int, payloads: List?) {
+
+ holder.bind(this)
+ }
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt
new file mode 100644
index 000000000..bb90f5307
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceDividerItemDecoration.kt
@@ -0,0 +1,47 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.support.v7.widget.RecyclerView
+import android.view.View
+
+class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
+
+ private val divider: Drawable
+
+ init {
+ val a = context.obtainStyledAttributes(ATTRS)
+ divider = a.getDrawable(0)
+ a.recycle()
+ }
+
+ override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
+ val left = parent.paddingLeft + SourceHolder.margin
+ val right = parent.width - parent.paddingRight - SourceHolder.margin
+
+ val childCount = parent.childCount
+ for (i in 0 until childCount - 1) {
+ val child = parent.getChildAt(i)
+ if (parent.getChildViewHolder(child) is SourceHolder &&
+ parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) {
+ val params = child.layoutParams as RecyclerView.LayoutParams
+ val top = child.bottom + params.bottomMargin
+ val bottom = top + divider.intrinsicHeight
+
+ divider.setBounds(left, top, right, bottom)
+ divider.draw(c)
+ }
+ }
+ }
+
+ override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView,
+ state: RecyclerView.State) {
+ outRect.set(0, 0, 0, divider.intrinsicHeight)
+ }
+
+ companion object {
+ private val ATTRS = intArrayOf(android.R.attr.listDivider)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt
new file mode 100644
index 000000000..ddc8914b0
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceHolder.kt
@@ -0,0 +1,107 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.os.Build
+import android.view.View
+import android.view.ViewGroup
+import eu.davidea.viewholders.FlexibleViewHolder
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.online.LoginSource
+import eu.kanade.tachiyomi.util.dpToPx
+import eu.kanade.tachiyomi.util.getRound
+import eu.kanade.tachiyomi.util.gone
+import eu.kanade.tachiyomi.util.visible
+import io.github.mthli.slice.Slice
+import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.view.*
+
+class SourceHolder(view: View, adapter: CatalogueMainAdapter) : FlexibleViewHolder(view, adapter) {
+
+ private val slice = Slice(itemView.card).apply {
+ setColor(adapter.cardBackground)
+ }
+
+ init {
+ itemView.source_browse.setOnClickListener {
+ adapter.browseClickListener.onBrowseClick(adapterPosition)
+ }
+
+ itemView.source_latest.setOnClickListener {
+ adapter.latestClickListener.onLatestClick(adapterPosition)
+ }
+ }
+
+ fun bind(item: SourceItem) {
+ val source = item.source
+ with(itemView) {
+ setCardEdges(item)
+
+ // Set source name
+ title.text = source.name
+
+ // Set circle letter image.
+ post {
+ image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
+ }
+
+ // If source is login, show only login option
+ if (source is LoginSource && !source.isLogged()) {
+ source_browse.setText(R.string.login)
+ source_latest.gone()
+ } else {
+ source_browse.setText(R.string.browse)
+ source_latest.visible()
+ }
+ }
+ }
+
+ private fun setCardEdges(item: SourceItem) {
+ // Position of this item in its header. Defaults to 0 when header is null.
+ var position = 0
+
+ // Number of items in the header of this item. Defaults to 1 when header is null.
+ var count = 1
+
+ if (item.header != null) {
+ val sectionItems = mAdapter.getSectionItems(item.header)
+ position = sectionItems.indexOf(item)
+ count = sectionItems.size
+ }
+
+ when {
+ // Only one item in the card
+ count == 1 -> applySlice(2f, false, false, true, true)
+ // First item of the card
+ position == 0 -> applySlice(2f, false, true, true, false)
+ // Last item of the card
+ position == count - 1 -> applySlice(2f, true, false, false, true)
+ // Middle item
+ else -> applySlice(0f, false, false, false, false)
+ }
+ }
+
+ private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
+ topShadow: Boolean, bottomShadow: Boolean) {
+
+ slice.setRadius(radius)
+ slice.showLeftTopRect(topRect)
+ slice.showRightTopRect(topRect)
+ slice.showLeftBottomRect(bottomRect)
+ slice.showRightBottomRect(bottomRect)
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ slice.showTopEdgeShadow(topShadow)
+ slice.showBottomEdgeShadow(bottomShadow)
+ }
+ setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
+ }
+
+ private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
+ val v = itemView.card
+ if (v.layoutParams is ViewGroup.MarginLayoutParams) {
+ val p = v.layoutParams as ViewGroup.MarginLayoutParams
+ p.setMargins(left, top, right, bottom)
+ }
+ }
+
+ companion object {
+ val margin = 8.dpToPx
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt
new file mode 100644
index 000000000..14593ab07
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/main/SourceItem.kt
@@ -0,0 +1,45 @@
+package eu.kanade.tachiyomi.ui.catalogue.main
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.davidea.flexibleadapter.items.AbstractSectionableItem
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.CatalogueSource
+
+/**
+ * Item that contains source information.
+ *
+ * @param source Instance of [CatalogueSource] containing source information.
+ * @param header The header for this item.
+ */
+data class SourceItem(val source: CatalogueSource, val header: LangItem? = null) :
+ AbstractSectionableItem(header) {
+
+ /**
+ * Returns the layout resource of this item.
+ */
+ override fun getLayoutRes(): Int {
+ return R.layout.catalogue_main_controller_card_item
+ }
+
+ /**
+ * Creates a new view holder for this item.
+ */
+ override fun createViewHolder(adapter: FlexibleAdapter<*>, inflater: LayoutInflater,
+ parent: ViewGroup): SourceHolder {
+
+ val view = inflater.inflate(layoutRes, parent, false)
+ return SourceHolder(view, adapter as CatalogueMainAdapter)
+ }
+
+ /**
+ * Binds this item to the given view holder.
+ */
+ override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder,
+ position: Int, payloads: List?) {
+
+ holder.bind(this)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
index 906bbb910..0dd3c1fa7 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/category/CategoryHolder.kt
@@ -7,6 +7,7 @@ import com.amulyakhare.textdrawable.TextDrawable
import com.amulyakhare.textdrawable.util.ColorGenerator
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.util.getRound
import kotlinx.android.synthetic.main.categories_item.view.*
/**
@@ -38,27 +39,10 @@ class CategoryHolder(view: View, val adapter: CategoryAdapter) : FlexibleViewHol
// Update circle letter image.
itemView.post {
- itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase()))
+ itemView.image.setImageDrawable(itemView.image.getRound(category.name.take(1).toUpperCase(),false))
}
}
- /**
- * Returns circle letter image.
- *
- * @param text The first letter of string.
- */
- private fun getRound(text: String): TextDrawable {
- val size = Math.min(itemView.image.width, itemView.image.height)
- return TextDrawable.builder()
- .beginConfig()
- .width(size)
- .height(size)
- .textColor(Color.WHITE)
- .useFont(Typeface.DEFAULT)
- .endConfig()
- .buildRound(text, ColorGenerator.MATERIAL.getColor(text))
- }
-
/**
* Called when an item is released.
*
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
index 730b5e991..072980607 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesController.kt
@@ -1,19 +1,25 @@
package eu.kanade.tachiyomi.ui.latest_updates
+import android.os.Bundle
import android.support.v4.widget.DrawerLayout
import android.view.Menu
import android.view.ViewGroup
import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
/**
- * Fragment that shows the manga from the catalogue. Inherit CatalogueFragment.
+ * Controller that shows the latest manga from the catalogue. Inherit [CatalogueController].
*/
-class LatestUpdatesController : CatalogueController() {
+class LatestUpdatesController(bundle: Bundle) : CatalogueController(bundle) {
+
+ constructor(source: CatalogueSource) : this(Bundle().apply {
+ putLong(SOURCE_ID_KEY, source.id)
+ })
override fun createPresenter(): CataloguePresenter {
- return LatestUpdatesPresenter()
+ return LatestUpdatesPresenter(args.getLong(SOURCE_ID_KEY))
}
override fun onPrepareOptionsMenu(menu: Menu) {
@@ -30,4 +36,4 @@ class LatestUpdatesController : CatalogueController() {
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
index 924425b62..2e0ea07fe 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/latest_updates/LatestUpdatesPresenter.kt
@@ -1,7 +1,5 @@
package eu.kanade.tachiyomi.ui.latest_updates
-import eu.kanade.tachiyomi.source.CatalogueSource
-import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
import eu.kanade.tachiyomi.ui.catalogue.Pager
@@ -9,18 +7,10 @@ import eu.kanade.tachiyomi.ui.catalogue.Pager
/**
* Presenter of [LatestUpdatesController]. Inherit CataloguePresenter.
*/
-class LatestUpdatesPresenter : CataloguePresenter() {
+class LatestUpdatesPresenter(sourceId: Long) : CataloguePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
return LatestUpdatesPager(source)
}
- override fun getEnabledSources(): List {
- return super.getEnabledSources().filter { it.supportsLatest }
- }
-
- override fun isValidSource(source: Source?): Boolean {
- return super.isValidSource(source) && (source as CatalogueSource).supportsLatest
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index d6bf1d233..7db5474c4 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -18,9 +18,8 @@ import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
-import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
+import eu.kanade.tachiyomi.ui.catalogue.main.CatalogueMainController
import eu.kanade.tachiyomi.ui.download.DownloadController
-import eu.kanade.tachiyomi.ui.latest_updates.LatestUpdatesController
import eu.kanade.tachiyomi.ui.library.LibraryController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
@@ -84,8 +83,7 @@ class MainActivity : BaseActivity() {
R.id.nav_drawer_library -> setRoot(LibraryController(), id)
R.id.nav_drawer_recent_updates -> setRoot(RecentChaptersController(), id)
R.id.nav_drawer_recently_read -> setRoot(RecentlyReadController(), id)
- R.id.nav_drawer_catalogues -> setRoot(CatalogueController(), id)
- R.id.nav_drawer_latest_updates -> setRoot(LatestUpdatesController(), id)
+ R.id.nav_drawer_catalogues -> setRoot(CatalogueMainController(), id)
R.id.nav_drawer_downloads -> {
router.pushController(RouterTransaction.with(DownloadController())
.pushChangeHandler(FadeChangeHandler())
@@ -250,4 +248,4 @@ class MainActivity : BaseActivity() {
const val SHORTCUT_MANGA = "eu.kanade.tachiyomi.SHOW_MANGA"
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
index 4953429fb..c548bb849 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt
@@ -30,12 +30,6 @@ class SettingsMainController : SettingsController() {
titleRes = R.string.pref_category_downloads
onClick { navigateTo(SettingsDownloadController()) }
}
- preference {
- iconRes = R.drawable.ic_language_black_24dp
- iconTint = tintColor
- titleRes = R.string.pref_category_sources
- onClick { navigateTo(SettingsSourcesController()) }
- }
preference {
iconRes = R.drawable.ic_sync_black_24dp
iconTint = tintColor
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt
index c7982d0d8..25f847b72 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsSourcesController.kt
@@ -3,6 +3,8 @@ package eu.kanade.tachiyomi.ui.setting
import android.graphics.drawable.Drawable
import android.support.v7.preference.PreferenceGroup
import android.support.v7.preference.PreferenceScreen
+import com.bluelinelabs.conductor.ControllerChangeHandler
+import com.bluelinelabs.conductor.ControllerChangeType
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt
index e37667f76..f2063e675 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt
@@ -105,7 +105,7 @@ val Context.powerManager: PowerManager
*
* @param intent intent that contains broadcast information
*/
-fun Context.sendLocalBroadcast(intent:Intent){
+fun Context.sendLocalBroadcast(intent: Intent) {
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt
index 912f05e80..0bd1ce9a2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/ViewExtensions.kt
@@ -4,9 +4,12 @@ package eu.kanade.tachiyomi.util
import android.graphics.Color
import android.graphics.Point
+import android.graphics.Typeface
import android.support.design.widget.Snackbar
import android.view.View
import android.widget.TextView
+import com.amulyakhare.textdrawable.TextDrawable
+import com.amulyakhare.textdrawable.util.ColorGenerator
/**
* Returns coordinates of view.
@@ -43,3 +46,21 @@ inline fun View.invisible() {
inline fun View.gone() {
visibility = View.GONE
}
+
+/**
+ * Returns a TextDrawable determined by input
+ *
+ * @param text text of [TextDrawable]
+ * @param random random color
+ */
+fun View.getRound(text: String, random : Boolean = true): TextDrawable {
+ val size = Math.min(this.width, this.height)
+ return TextDrawable.builder()
+ .beginConfig()
+ .width(size)
+ .height(size)
+ .textColor(Color.WHITE)
+ .useFont(Typeface.DEFAULT)
+ .endConfig()
+ .buildRound(text, if (random) ColorGenerator.MATERIAL.randomColor else ColorGenerator.MATERIAL.getColor(text))
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml
index 4ad2729af..5eab2ca69 100644
--- a/app/src/main/res/drawable-v21/library_item_selector_amoled.xml
+++ b/app/src/main/res/drawable-v21/library_item_selector_amoled.xml
@@ -18,6 +18,4 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/library_item_selector_dark.xml b/app/src/main/res/drawable-v21/library_item_selector_dark.xml
index e78c6ec16..82a72da4a 100644
--- a/app/src/main/res/drawable-v21/library_item_selector_dark.xml
+++ b/app/src/main/res/drawable-v21/library_item_selector_dark.xml
@@ -18,6 +18,4 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/library_item_selector_light.xml b/app/src/main/res/drawable-v21/library_item_selector_light.xml
index c85ee3913..1f2e8bf89 100644
--- a/app/src/main/res/drawable-v21/library_item_selector_light.xml
+++ b/app/src/main/res/drawable-v21/library_item_selector_light.xml
@@ -18,6 +18,4 @@
-
-
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml
index 96494d93a..0fce81a34 100644
--- a/app/src/main/res/drawable-v21/list_item_selector_amoled.xml
+++ b/app/src/main/res/drawable-v21/list_item_selector_amoled.xml
@@ -1,6 +1,6 @@
+ android:color="@color/rippleColorDark">
-
-
diff --git a/app/src/main/res/drawable-v21/list_item_selector_dark.xml b/app/src/main/res/drawable-v21/list_item_selector_dark.xml
index 07ed74dd4..07b9ef6d5 100644
--- a/app/src/main/res/drawable-v21/list_item_selector_dark.xml
+++ b/app/src/main/res/drawable-v21/list_item_selector_dark.xml
@@ -1,6 +1,6 @@
+ android:color="@color/rippleColorDark">
-
-
diff --git a/app/src/main/res/drawable-v21/list_item_selector_light.xml b/app/src/main/res/drawable-v21/list_item_selector_light.xml
index 692d94f2b..942446ef0 100644
--- a/app/src/main/res/drawable-v21/list_item_selector_light.xml
+++ b/app/src/main/res/drawable-v21/list_item_selector_light.xml
@@ -1,6 +1,6 @@
+ android:color="@color/rippleColorLight">
-
-
diff --git a/app/src/main/res/drawable-v21/list_item_selector_trans.xml b/app/src/main/res/drawable-v21/list_item_selector_trans.xml
new file mode 100644
index 000000000..2c8d1001b
--- /dev/null
+++ b/app/src/main/res/drawable-v21/list_item_selector_trans.xml
@@ -0,0 +1,6 @@
+
+
-
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_search_black_112dp.xml b/app/src/main/res/drawable/ic_search_black_112dp.xml
new file mode 100644
index 000000000..05705a607
--- /dev/null
+++ b/app/src/main/res/drawable/ic_search_black_112dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/library_item_selector_amoled.xml b/app/src/main/res/drawable/library_item_selector_amoled.xml
index 92cb0db94..1cf05bdc9 100644
--- a/app/src/main/res/drawable/library_item_selector_amoled.xml
+++ b/app/src/main/res/drawable/library_item_selector_amoled.xml
@@ -1,10 +1,10 @@
+ xmlns:android="http://schemas.android.com/apk/res/android">
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/library_item_selector_dark.xml b/app/src/main/res/drawable/library_item_selector_dark.xml
index 73de4df07..9880c4b38 100644
--- a/app/src/main/res/drawable/library_item_selector_dark.xml
+++ b/app/src/main/res/drawable/library_item_selector_dark.xml
@@ -1,10 +1,10 @@
+ xmlns:android="http://schemas.android.com/apk/res/android">
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/library_item_selector_light.xml b/app/src/main/res/drawable/library_item_selector_light.xml
index 9273e00fe..70f7b85b4 100644
--- a/app/src/main/res/drawable/library_item_selector_light.xml
+++ b/app/src/main/res/drawable/library_item_selector_light.xml
@@ -1,19 +1,10 @@
-
-
-
-
-
-
-
-
-
+ xmlns:android="http://schemas.android.com/apk/res/android">
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/list_item_selector_amoled.xml b/app/src/main/res/drawable/list_item_selector_amoled.xml
index e573c82bb..9bbf56578 100644
--- a/app/src/main/res/drawable/list_item_selector_amoled.xml
+++ b/app/src/main/res/drawable/list_item_selector_amoled.xml
@@ -1,6 +1,6 @@
+ android:exitFadeDuration="@android:integer/config_longAnimTime">
diff --git a/app/src/main/res/drawable/list_item_selector_dark.xml b/app/src/main/res/drawable/list_item_selector_dark.xml
index dd0779885..60034f818 100644
--- a/app/src/main/res/drawable/list_item_selector_dark.xml
+++ b/app/src/main/res/drawable/list_item_selector_dark.xml
@@ -1,6 +1,6 @@
+ android:exitFadeDuration="@android:integer/config_longAnimTime">
diff --git a/app/src/main/res/drawable/list_item_selector_light.xml b/app/src/main/res/drawable/list_item_selector_light.xml
index 73e54c92f..92bed9fc9 100644
--- a/app/src/main/res/drawable/list_item_selector_light.xml
+++ b/app/src/main/res/drawable/list_item_selector_light.xml
@@ -1,15 +1,6 @@
-
-
-
-
-
-
-
-
-
+ android:exitFadeDuration="@android:integer/config_longAnimTime">
diff --git a/app/src/main/res/drawable/list_item_selector_trans.xml b/app/src/main/res/drawable/list_item_selector_trans.xml
new file mode 100644
index 000000000..f61aa8a0d
--- /dev/null
+++ b/app/src/main/res/drawable/list_item_selector_trans.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/text_button.xml b/app/src/main/res/drawable/text_button.xml
new file mode 100644
index 000000000..ef5c24c56
--- /dev/null
+++ b/app/src/main/res/drawable/text_button.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_controller.xml b/app/src/main/res/layout/catalogue_controller.xml
index 48d8601ba..17ba20e10 100644
--- a/app/src/main/res/layout/catalogue_controller.xml
+++ b/app/src/main/res/layout/catalogue_controller.xml
@@ -6,12 +6,12 @@
android:layout_height="match_parent">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true"
+ android:orientation="vertical"
+ android:id="@+id/catalogue_view"
+ tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueController">
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_global_search_controller_card.xml b/app/src/main/res/layout/catalogue_global_search_controller_card.xml
new file mode 100644
index 000000000..db1a39ca0
--- /dev/null
+++ b/app/src/main/res/layout/catalogue_global_search_controller_card.xml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_global_search_controller_card_item.xml b/app/src/main/res/layout/catalogue_global_search_controller_card_item.xml
new file mode 100644
index 000000000..396ff7779
--- /dev/null
+++ b/app/src/main/res/layout/catalogue_global_search_controller_card_item.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_grid_item.xml b/app/src/main/res/layout/catalogue_grid_item.xml
index ee76f80b1..e475733d4 100644
--- a/app/src/main/res/layout/catalogue_grid_item.xml
+++ b/app/src/main/res/layout/catalogue_grid_item.xml
@@ -5,7 +5,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:background="?attr/selectable_library_drawable">
+ android:background="?selectable_library_drawable">
+ android:paddingStart="@dimen/material_component_lists_icon_left_padding"/>
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_main_controller_card.xml b/app/src/main/res/layout/catalogue_main_controller_card.xml
new file mode 100644
index 000000000..aec409b0a
--- /dev/null
+++ b/app/src/main/res/layout/catalogue_main_controller_card.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_main_controller_card_item.xml b/app/src/main/res/layout/catalogue_main_controller_card_item.xml
new file mode 100644
index 000000000..4f996dc84
--- /dev/null
+++ b/app/src/main/res/layout/catalogue_main_controller_card_item.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/catalogue_recycler_autofit.xml b/app/src/main/res/layout/catalogue_recycler_autofit.xml
index a6e103793..5ba8ec8dd 100644
--- a/app/src/main/res/layout/catalogue_recycler_autofit.xml
+++ b/app/src/main/res/layout/catalogue_recycler_autofit.xml
@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/catalogue_grid"
- style="@style/Theme.Widget.GridView"
+ style="@style/Theme.Widget.GridView.Catalogue"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnWidth="140dp"
diff --git a/app/src/main/res/layout/categories_item.xml b/app/src/main/res/layout/categories_item.xml
index 42cbe12af..681f229a0 100644
--- a/app/src/main/res/layout/categories_item.xml
+++ b/app/src/main/res/layout/categories_item.xml
@@ -15,7 +15,8 @@
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
android:paddingStart="@dimen/material_component_lists_icon_left_padding"
android:paddingRight="0dp"
- android:paddingEnd="0dp"/>
+ android:paddingEnd="0dp"
+ tools:src="@mipmap/ic_launcher_round"/>
+
+
+
+
+
diff --git a/app/src/main/res/menu/catalogue_new_list.xml b/app/src/main/res/menu/catalogue_new_list.xml
new file mode 100644
index 000000000..a528ba8e4
--- /dev/null
+++ b/app/src/main/res/menu/catalogue_new_list.xml
@@ -0,0 +1,11 @@
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8fed988aa..47fcc3a5a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -34,6 +34,7 @@
Last read
Last updated
Search
+ Global search
Select all
Mark as read
Mark as unread
@@ -85,6 +86,8 @@
Open log
Create
Restore
+ Open
+ Login
Deleting…
@@ -276,8 +279,13 @@
Please enable at least one valid source
No more results
Local manga
+ Other
Default can\'t be selected with other categories
The manga has been added to your library
+ Global search…
+ No results found!
+ Latest
+ Browse
This manga was removed from the database!
@@ -430,5 +438,4 @@
No wifi connection available
No network connection available
Download paused
-
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 66e4210d4..a70b29ba3 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -4,7 +4,7 @@
-
+
+
+
@@ -105,6 +109,10 @@
- 20sp
+
+
@@ -130,7 +138,7 @@
-
+
@@ -161,21 +175,24 @@
+
+
-
+
-
+
-